Gonkey - Ferramenta de Teste de Microsserviços

O Gonkey está testando nossos microsserviços em Lamoda e achamos que poderia testar o seu, então o colocamos em código aberto . Se a funcionalidade de seus serviços for implementada principalmente por meio da API e o JSON for usado para troca de dados, o Gonkey quase certamente será adequado para você.


imagem


Abaixo, falarei sobre isso em mais detalhes e mostrarei com exemplos específicos como usá-lo.


Como Gonkey nasceu


Temos mais de cem microsserviços, cada um dos quais resolve uma tarefa específica. Todos os serviços têm uma API. Obviamente, alguns deles também são uma interface de usuário, mas, no entanto, sua função principal é ser uma fonte de dados para um site, aplicativos móveis ou outros serviços internos e, portanto, fornecer uma interface de software .


Quando percebemos que havia muitos serviços, e então haveria ainda mais deles, desenvolvemos um documento interno descrevendo a abordagem padrão para o design da API e tomamos o Swagger como uma ferramenta de descrição (e até escrevemos utilitários para gerar código com base na especificação do swagger). Se você estiver interessado em aprender mais sobre isso, consulte a conversa de Andrew com o Highload ++.


A abordagem padrão para o design da API naturalmente levou à idéia de uma abordagem padrão para testes. Aqui está o que eu queria alcançar:


  1. Teste os serviços por meio da API , porque quase toda a funcionalidade do serviço é implementada por meio dela
  2. A capacidade de automatizar o lançamento de testes para integrá-lo ao nosso processo de CI / CD, como se costuma dizer, "executado por botão"
  3. Escrever testes deve ser alienável , ou seja, para que não apenas um programador possa escrever testes, de preferência uma pessoa que não esteja familiarizada com a programação.

Então Gonkey nasceu.


Então o que é isso?


O Gonkey é uma biblioteca (para projetos no Golang) e um utilitário de console (para projetos em qualquer idioma e tecnologia), com o qual você pode executar testes funcionais e de regressão de serviços acessando sua API de acordo com um script predefinido. Os scripts de teste são descritos nos arquivos YAML.


Simplificando, o Gonkey pode:


  • bombardeie seu serviço com solicitações HTTP e garanta que suas respostas sejam as esperadas. Ele pressupõe que o JSON seja usado em solicitações e respostas, mas provavelmente funcionará em casos simples com respostas em um formato diferente;
  • prepare o banco de dados para o teste preenchendo-o com dados de equipamentos (também especificados nos arquivos YAML);
  • imitar as respostas de serviços externos usando zombarias (esse recurso estará disponível apenas se você conectar o Gonkey como uma biblioteca);
  • forneça resultados de teste ao console ou gere um relatório Allure.

Repositório do projeto
Imagem do Docker


Exemplo de teste de serviço com Gonkey


Para não sobrecarregar o texto, quero passar de palavras para ações e testar alguma API aqui e, ao longo do caminho, informar e mostrar como os scripts de teste são escritos.


Vamos esboçar um pequeno serviço no Go que simulará o trabalho de um semáforo. Ele armazena a cor do sinal atual: vermelho, amarelo ou verde. Você pode obter a cor do sinal atual ou definir uma nova por meio da API.


//    const ( lightRed = "red" lightYellow = "yellow" lightGreen = "green" ) //      type trafficLights struct { currentLight string `json:"currentLight"` mutex sync.RWMutex `json:"-"` } //   var lights = trafficLights{ currentLight: lightRed, } func main() { //       http.HandleFunc("/light/get", func(w http.ResponseWriter, r *http.Request) { lights.mutex.RLock() defer lights.mutex.RUnlock() resp, err := json.Marshal(lights) if err != nil { log.Fatal(err) } w.Write(resp) }) //       http.HandleFunc("/light/set", func(w http.ResponseWriter, r *http.Request) { lights.mutex.Lock() defer lights.mutex.Unlock() request, err := ioutil.ReadAll(r.Body) if err != nil { log.Fatal(err) } var newTrafficLights trafficLights if err := json.Unmarshal(request, &newTrafficLights); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := validateRequest(&newTrafficLights); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } lights = newTrafficLights }) //   () log.Fatal(http.ListenAndServe(":8080", nil)) } func validateRequest(lights *trafficLights) error { if lights.currentLight != lightRed && lights.currentLight != lightYellow && lights.currentLight != lightGreen { return fmt.Errorf("incorrect current light: %s", lights.currentLight) } return nil } 

O código fonte completo do main.go está aqui .


Execute o programa:


 go run . 

Esboçado muito rápido em 15 minutos! Certamente ele estava enganado em algum lugar, então escreveremos um teste e verificaremos.


Faça o download e execute o Gonkey:


 mkdir -p tests/cases docker run -it -v $(pwd)/tests:/tests lamoda/gonkey -tests tests/cases -host host.docker.internal:8080 

Este comando inicia a imagem com gonkey através da janela de encaixe, monta o diretório tests / cases dentro do contêiner e inicia o gonkey com os parâmetros -tests tests / cases / -host.


Se você não gostar da abordagem do docker, uma alternativa a esse comando seria escrever:


 go get github.com/lamoda/gonkey go run github.com/lamoda/gonkey -tests tests/cases -host localhost:8080 

Lançado e obteve o resultado:


 Failed tests: 0/0 

Sem testes - nada para verificar. Vamos escrever o primeiro teste. Crie um arquivo tests / cases / light_get.yaml com o conteúdo mínimo:


 - name: WHEN currentLight is requested MUST return red method: GET path: /light/get response: 200: > { "currentLight": "red" } 

No primeiro nível, há uma lista. Isso significa que descrevemos um caso de teste, mas pode haver muitos deles no arquivo. Juntos, eles compõem o cenário de teste. Assim, um arquivo - um script. Você pode criar qualquer número de arquivos com scripts de teste, se conveniente, organizá-los em subdiretórios - o gonkey lê todos os arquivos yaml e yml do diretório transferido e é mais recursivo.


O arquivo abaixo descreve os detalhes da solicitação que será enviada ao servidor: método, caminho. Ainda mais baixo é o código de resposta (200) e o corpo de resposta que esperamos do servidor.


O formato completo do arquivo é descrito em README .


Execute novamente:


 docker run -it -v $(pwd)/tests:/tests lamoda/gonkey -tests tests/cases -host host.docker.internal:8080 

Resultado:


  Name: WHEN currentlight is requested MUST return red Request: Method: GET Path: /light/get Query: Body: <no body> Response: Status: 200 OK Body: {} Result: ERRORS! Errors: 1) at path $ values do not match: expected: { "currentLight": "red" } actual: {} Failed tests: 1/1 

Erro! Era esperada uma estrutura com o campo currentLight e retornada uma estrutura vazia. Isso é ruim. O primeiro problema é que o resultado foi interpretado como uma string, isso é indicado pelo fato de que, como local do problema, o gonkey destacou a resposta inteira sem nenhum detalhe:


  expected: { "currentLight": "red" } 

O motivo é simples: eu esqueci de escrever para que o serviço na resposta indique o tipo de conteúdo application / json. Nós corrigimos:


 //       http.HandleFunc("/light/get", func(w http.ResponseWriter, r *http.Request) { lights.mutex.RLock() defer lights.mutex.RUnlock() resp, err := json.Marshal(lights) if err != nil { log.Fatal(err) } w.Header().Add("Content-Type", "application/json") // <--  w.Write(resp) }) 

Reiniciamos o serviço e executamos os testes novamente:


  Name: WHEN currentlight is requested MUST return red Request: Method: GET Path: /light/get Query: Body: <no body> Response: Status: 200 OK Body: {} Result: ERRORS! Errors: 1) at path $ key is missing: expected: currentLight actual: <missing> 

Ótimo, há progresso. Gonkey agora reconhece a estrutura, mas ainda está incorreta: a resposta está vazia. O motivo é que usei um campo currentLight não exportável na definição de tipo:


 //      type trafficLights struct { currentLight string `json:"currentLight"` mutex sync.RWMutex `json:"-"` } 

No Go, um campo de estrutura nomeado com uma letra minúscula é considerado não exportável, ou seja, inacessível a partir de outros pacotes. O serializador JSON não o vê e não pode incluí-lo na resposta. Nós corrigimos: fazemos o campo com uma letra maiúscula, o que significa que ele é exportado:


 //      type trafficLights struct { urrentLight string `json:"currentLight"` // <--   mutex sync.RWMutex `json:"-"` } 

Reinicie o serviço. Execute os testes novamente.


 Failed tests: 0/1 

Os testes passaram!


Escreveremos outro script que testará o método set. Preencha o arquivo tests / cases / light_set.yaml com o seguinte conteúdo:


 - name: WHEN set is requested MUST return no response method: POST path: /light/set request: > { "currentLight": "green" } response: 200: '' - name: WHEN get is requested MUST return green method: GET path: /light/get response: 200: > { "currentLight": "green" } 

O primeiro teste define um novo valor para o sinal de trânsito e o segundo verifica o status para garantir que ele tenha sido alterado.


Execute os testes com o mesmo comando:


 docker run -it -v $(pwd)/tests:/tests lamoda/gonkey -tests tests/cases -host host.docker.internal:8080 

Resultado:


 Failed tests: 0/3 

Um resultado bem-sucedido, mas tivemos sorte que os scripts foram executados na ordem que precisávamos: primeiro light_get e, em seguida, light_set. O que aconteceria se eles fizessem o oposto? Vamos renomear:


 mv tests/cases/light_set.yaml tests/cases/_light_set.yaml 

E corra novamente:


 Errors: 1) at path $.currentLight values do not match: expected: red actual: green Failed tests: 1/3 

Primeiro, o conjunto foi executado e o semáforo foi deixado no estado verde; portanto, o teste de execução seguinte encontrou um erro - ele estava esperando o vermelho.


Uma maneira de se livrar do fato de que o teste depende do contexto é inicializar o serviço no início do script (ou seja, no início do arquivo), o que geralmente fazemos no teste definido - primeiro, definimos o valor conhecido, que deve produzir um efeito conhecido, e depois verifique se o efeito teve efeito.


Outra maneira de preparar o contexto de execução se o serviço usar o banco de dados é usar equipamentos com dados que são carregados no banco de dados no início do script, formando assim um estado previsível do serviço que pode ser verificado. A descrição e exemplos de como trabalhar com acessórios no gonkey eu quero publicar em outro artigo.


Enquanto isso, proponho a seguinte solução. Como no script set estamos realmente testando os métodos light / set e light / get, simplesmente não precisamos do script light_get, que é sensível ao contexto. Eu o apago e renomeio o script restante para que o nome reflita a essência.


 rm tests/cases/light_get.yaml mv tests/cases/_light_set.yaml tests/cases/light_set_get.yaml 

Como próximo passo, gostaria de verificar alguns cenários negativos de trabalho com nosso serviço, por exemplo, funcionará corretamente se enviar uma cor de sinal incorreta? Ou não envia cores?


Crie um novo script tests / cases / light_set_get_negative.yaml:


 - name: WHEN set is requested MUST return no response method: POST path: /light/set request: > { "currentLight": "green" } response: 200: '' - name: WHEN incorrect color is passed MUST return error method: POST path: /light/set request: > { "currentLight": "blue" } response: 400: > incorrect current light: blue - name: WHEN color is missing MUST return error method: POST path: /light/set request: > {} response: 400: > incorrect current light: - name: WHEN get is requested MUST have color untouched method: GET path: /light/get response: 200: > { "currentLight": "green" } 

Ele verifica que:


  • quando a cor errada é transmitida, ocorre um erro;
  • quando a cor não é transmitida, ocorre um erro;
  • a transmissão incorreta de cores não altera o estado interno do semáforo.

Execute:


 Failed tests: 0/6 

Está tudo bem :)


Conectando o Gonkey como uma biblioteca


Como você notou, estamos testando a API de serviço, abstraindo completamente a linguagem e as tecnologias em que está escrita. Da mesma forma, poderíamos testar qualquer API pública para a qual não temos acesso aos códigos-fonte - basta enviar solicitações e receber respostas.


Porém, para nossos próprios aplicativos escritos em go, existe uma maneira mais conveniente de executar o gonkey - conectá-lo ao projeto como uma biblioteca. Isso permitirá, sem compilar nada com antecedência - nem o gonkey, nem o próprio projeto - executar o teste simplesmente executando go test .


Com essa abordagem, parece que começamos a escrever um teste de unidade e, no corpo do teste, fazemos o seguinte:


  • inicialize o servidor da web da mesma maneira que quando o serviço é iniciado;
  • execute o servidor de aplicativos de teste no host local e na porta aleatória;
  • chamamos a função da biblioteca gonkey, passando o endereço do servidor de teste e outros parâmetros. Abaixo vou ilustrar isso.

Para fazer isso, nosso aplicativo precisará de um pouco de refatoração. Seu ponto principal é tornar a criação do servidor uma função separada, porque agora precisamos dessa função em dois locais: quando o serviço é iniciado e mesmo quando os testes do gonkey são executados.


Coloquei o seguinte código em uma função separada:


 func initServer() { //       http.HandleFunc("/light/get", func(w http.ResponseWriter, r *http.Request) { //   }) //       http.HandleFunc("/light/set", func(w http.ResponseWriter, r *http.Request) { //   }) } 

A função principal será assim:


 func main() { initServer() //   () log.Fatal(http.ListenAndServe(":8080", nil)) } 

O arquivo principal principal modificado completamente .


Isso liberou nossas mãos, então vamos começar a escrever um teste. Eu crio um arquivo func_test.go:


 func Test_API(t *testing.T) { initServer() srv := httptest.NewServer(nil) runner.RunWithTesting(t, &runner.RunWithTestingParams{ Server: srv, TestsDir: "tests/cases", }) } 

Aqui está o arquivo func_test.go completo .


Isso é tudo! Verificamos:


 go test ./... 

Resultado:


 ok github.com/lamoda/gonkey/examples/traffic-lights-demo 0.018s 

Os testes passaram. Se eu tiver testes de unidade e de gonkey, eles serão executados todos juntos - de maneira bastante conveniente.


Gere um relatório Allure


Allure é um formato de relatório de teste para exibir resultados de uma maneira clara e bonita. Gonkey pode registrar os resultados dos testes neste formato. A ativação do Allure é muito simples:


 docker run -it -v $(pwd)/tests:/tests -w /tests lamoda/gonkey -tests cases/ -host host.docker.internal:8080 -allure 

O relatório será colocado no subdiretório allure-results do diretório de trabalho atual (foi por isso que especifiquei -w / tests).


Ao conectar o gonkey como uma biblioteca, o relatório Allure é ativado configurando uma variável de ambiente adicional GONKEY_ALLURE_DIR:


 GONKEY_ALLURE_DIR="tests/allure-results" go test ./… 

Os resultados do teste registrados nos arquivos são convertidos em um relatório interativo pelos comandos:


 allure generate allure serve 

Como é o relatório:
imagem


Conclusão


Nos artigos a seguir, abordarei o uso de acessórios no gonkey e imitarei as respostas de outros serviços usando zombarias.


Convido você a experimentar o gonkey em seus projetos, participar de seu desenvolvimento (solicitações de pool são bem-vindas!) Ou marque-o com um asterisco no github se este projeto puder ser útil para você no futuro.

Source: https://habr.com/ru/post/pt463301/


All Articles