Um bom desenvolvedor sempre testa seu código, no entanto, os métodos de teste comuns podem ser muito simplistas em alguns casos. Dependendo da complexidade de um projeto, pode ser necessário executar testes avançados para avaliar com precisão o desempenho do seu código.
Neste artigo, examinaremos alguns padrões de teste em Go que o ajudarão escrever testes eficazes para qualquer projeto. Abordaremos conceitos como simulação, acessórios de teste, auxiliares de teste e arquivos dourados, e você verá como pode aplicar cada técnica em um cenário do mundo real.
Para acompanhar este artigo, você deve ter conhecimento prévio de teste de unidade em Go . Vamos começar!
Testando manipuladores HTTP
Primeiro, vamos considerar um cenário comum, o teste de manipuladores HTTP. Os manipuladores HTTP devem ser fracamente acoplados a suas dependências, facilitando o isolamento de um elemento para teste sem impactar o resto do código. Se seus manipuladores HTTP forem bem projetados inicialmente, o teste deve ser bastante direto.
Verificando o código de status
Vamos considerar um teste básico que verifica o código de status do seguinte manipulador HTTP:
func index (w http.ResponseWriter, r * http.Request) {w.WriteHeader (http.StatusOK)}
O manipulador index () acima deve retornar uma resposta 200 OK para cada solicitação. Vamos verificar a resposta do manipulador com o seguinte teste:
func TestIndexHandler (t * testing.T) {w:=httptest.NewRecorder () r:=httptest.NewRequest (http.MethodGet,”/”, nil) index (w, r) if w.Code!=http.StatusOK {t.Errorf (“Status esperado:% d, mas obtido:% d”, http.StatusOK, w.Code)}}
No snippet de código acima, usamos o pacote httptest para testar o manipulador index (). Retornamos um httptest.ResponseRecorder, que implementa a interface http.ResponseWriter por meio do método NewRecorder (). http.ResponseWriter registra quaisquer mutações, permitindo-nos fazer afirmações no teste.
Também podemos criar uma solicitação HTTP usando o método httptest.NewRequest (). Isso especifica os tipos de solicitações esperadas pelo manipulador, como o método da solicitação, os parâmetros da consulta e o corpo da resposta. Você também pode definir cabeçalhos de solicitação após obter o objeto http.Request por meio do tipo http.Header.
Depois de chamar o manipulador index () com o objeto http.Request e o gravador de resposta, você pode inspecionar diretamente o manipulador resposta usando a propriedade Code. Para fazer afirmações sobre outras propriedades na resposta, como os cabeçalhos ou o corpo, você pode acessar o método ou propriedade apropriada no gravador de resposta:
$ go test-v===RUN TestIndexHandler—PASS: TestIndexHandler (0,00s) PASSAR ok github.com/ayoisaiah/random 0,004s
Dependências externas
Agora, vamos considerar outro cenário comum em que nosso gerenciador de HTTP depende de um serviço externo:
func getJoke (w http.ResponseWriter, r * http.Request) {u, err:=url.Parse (r.URL.String ()) if err!=nil {http.Error (w, err.Error () , http.StatusInternalServerError) return} jokeId:=u.Query (). Get (“id”) if jokeId==””{http.Error (w,”Joke ID não pode estar vazio”, http.StatusBadRequest) return} endpoint:=”https://icanhazdadjoke.com/j/”+ jokeId client:=http.Client {Timeout: 10 * time.Second,} req, err:=http.NewRequest (http.MethodGet, endpoint, nil) se err!=nulo {http.Error (w, err.Error (), http.StatusInternalServerError) return} req.Header.Set (“Aceitar”,”text/plain”) resp, err:=client.Do (req) if err!=nil {http.Error (w, err.Error (), http.StatusInternalServerError) return} adiar resp.Body.Close () b, err:=ioutil.ReadAll (resp.Body) if err!=nil {http.Error (w, err.Error (), http.StatusInternalServerError) return} if resp.StatusCode!=http.StatusOK {http.Error (w, string (b), resp.StatusCode) return} w.Header (). Set (“Content-Type”,”text/plain”) w.WriteHeader (http.StatusOK) w.Write (b)} func main () {mux:=http.NewServeMux ()
icanhazdadjoke
Para o cara que inventou o zero… obrigado por nada.
mux.HandleFunc (“/joke”, getJoke) http.ListenAndServe (“: 1212”, mux)}
No bloco de código acima, o manipulador getJoke espera um parâmetro de consulta n id, que ele usa para buscar uma piada na API de piada aleatória do pai.
Vamos escrever um teste para este manipulador:
func TestGetJokeHandler (t * testing.T) {table:=[] struct {id string statusCode int body string} {{“R7UfaahVfFd”, 200,”Meu cachorro costumava perseguir pessoas em uma bicicleta a muito. Ficou tão ruim que tive que tirar a bicicleta dele.”}, {“173782”, 404,` Piada com id”173782″not found`}, {“”, 400,”Joke ID não pode estar vazio”},} para _, v:=tabela de intervalo {t.Run (v.id, func (t * testing.T) {w:=httptest.NewRecorder () r:=httptest.NewRequest (http.MethodGet,”/joke? id=”+ v.id, nil) getJoke (w, r) if w.Code!=v.statusCode {t.Fatalf (“Código de status esperado:% d, mas obteve:% d”, v.statusCode, w. Code)} body:=strings.TrimSpace (w.Body.String ()) if body!=V.body {t.Fatalf (“Corpo esperado para ser:’% s’, mas obteve:’% s’”, v.body, body)}})}}
Usamos testes baseados em tabela para testar o manipulador em relação a uma variedade de entradas. A primeira entrada é um Joke ID válido que deve retornar uma resposta 200 OK. A segunda é inválida ID que deve retornar uma resposta 404. A entrada final é um ID vazio que deve retornar 400 solicitações incorretas resposta t.
Quando você executa o teste, ele deve passar com sucesso:
$ go test-v===RUN TestGetJokeHandler===RUN TestGetJokeHandler/R7UfaahVfFd===RUN TestGetJokeHandler/173782===EXECUTAR TestGetJokeHandler/# 00—PASSAR: TestGetJokeHandler (1.49s)—PASSAR: TestGetJokeHandler/R7UfaahVfFd (1.03s)—PASSAR: TestGetJokeHandler/173782 (0.47s)—PASSAR: TestGetJokeHandler/# 00 (0.00s) PASS ok github.com/ayoisaiah/random 1.498s
Observe que o teste no bloco de código acima faz solicitações HTTP para a API real. Fazer isso afeta as dependências do código que está sendo testado, o que é uma prática ruim para código de teste de unidade.
Em vez disso, devemos simular o cliente HTTP. Temos vários métodos diferentes para simular no Go, que exploraremos a seguir.
Simular no Go
Um padrão bastante simples para simular um cliente HTTP no Go é criar um padrão interface. Nossa interface definirá os métodos usados em uma função e passará diferentes implementações dependendo de onde a função é chamada.
A interface personalizada para nosso cliente HTTP acima deve ser semelhante ao seguinte bloco de código:
type HTTPClient interface {Do (req * http.Request) (* http.Response, error)}
Nossa assinatura para getJoke () será semelhante ao bloco de código abaixo:
func getJoke (client HTTPClient) http. HandlerFunc {return func (w http.ResponseWriter, r * http.Request) {//resto da função}}
O corpo original do manipulador getJoke () é movido para dentro do valor de retorno. A declaração da variável do cliente é removida do corpo em favor da interface HTTPClient.
A interface HTTPClient envolve um método Do (), que aceita uma solicitação HTTP e retorna uma resposta HTTP e um erro.
Precisamos fornecer uma implementação concreta de HTTPClient quando chamamos getJoke () na função main ():
func main () {mux:=http.NewServeMux () client:=http.Client { Tempo limite: 10 * time.Segundo,} mux.HandleFunc (“/joke”, getJoke (& client)) http.ListenAndServe (“: 1212”, mux)}
O tipo http.Client implementa a interface HTTPClient, então o O programa continua a chamar a API Random dad joke. Precisamos atualizar os testes com uma implementação HTTPClient diferente que não faça solicitações HTTP pela rede.
Primeiro, criaremos uma implementação simulada da interface HTTPClient:
type MockClient struct { Função DoFunc (req * http.Request) (* http.Response, erro)} função (m * MockClient) Do (req * http.Request) (* http.Response, erro) {return m.DoFunc (req)}
No bloco de código acima, a estrutura MockClient implementa a interface HTTPClient por meio de seu fornecimento do método Do, que chama uma propriedade DoFunc. Agora, precisamos implementar a função DoFunc quando criamos uma instância de MockClient no teste:
func TestGetJokeHandler (t * testing.T) {table:=[] struct {id string statusCode int body string} {{“R7UfaahVfFd”, 200,”Meu cachorro costumava perseguir pessoas em uma bicicleta. Ficou tão ruim que tive que tirar a bicicleta dele.”}, {“173782”, 404, `Piada com a id”173782″não encontrada `}, {“”, 400,”Joke ID não pode estar vazio”},} para _, v:=tabela de intervalo {t.Run (v.id, func (t * testing.T) {w:=httptest. NewRecorder () r:=httptest.NewRequest (http.MethodGet,”/joke?id=”+v.id, nil) c:=& MockClient {} c.DoFunc=func (req * http.Request) (* http. Resposta, erro) {return & http.Response {Body: io.NopCloser (strings.NewReader (v.body)), StatusCode: v.statusCode,}, nil} getJoke (c) (w, r ) if w.Code!=v.statusCode {t.Fatalf (“Código de status esperado:% d, mas obtido:% d”, v.statusCode, w.Code)} corpo:=strings.TrimSpace (w.Body. String ()) if body!=V.body {t.Fatalf (“Corpo esperado para ser:’% s’, mas obteve:’% s’”, v.body, body)}})}}
No trecho de código acima, DoFunc é ajustado para cada caso de teste, portanto, ele retorna uma resposta personalizada. Agora, evitamos todas as chamadas de rede, então o teste passará em uma taxa muito mais rápida:
$ go test-v===RUN TestGetJokeHandler===RUN TestGetJokeHandler/R7UfaahVfFd===RUN TestGetJokeHandler/173782===EXECUTAR TestGetJokeHandler/# 00—PASSAR: TestGetJokeHandler (0,00s)—PASSAR: TestGetJokeHandler/R7UfaahVfFd (0,00s)—PASSAR: TestGetJokeHandler/173782 (0,00s)—PASSAR: TestGetJokeHandler/# 00 (0,00s) PASSOU ok github.com/ayoisaiah/random 0,005s
Você pode usar este mesmo princípio quando seu manipulador depende de outro sistema externo, como um banco de dados. Desacoplar o manipulador de qualquer implementação específica permite que você simule facilmente a dependência no teste enquanto retém a implementação real no código do seu aplicativo.
Usando dados externos em testes
No Go, você deve colocar dados externos para testes em um diretório chamado testdata. Quando você constrói binários para seus programas, o diretório testdata é ignorado, então você pode usar essa abordagem para armazenar entradas com as quais deseja testar seu programa.
Por exemplo, vamos escrever uma função que gere a base64 codificação de um arquivo binário:
func getBase64Encoding (b [] byte) string {return base64.StdEncoding.EncodeToString (b)}
Para testar se esta função produz a saída correta, vamos colocar alguns arquivos de amostra e seus codificação base64 correspondente em um diretório testdata na raiz de nosso projeto:
$ ls testdata img1.jpg img1_base64.txt img2.jpg img2_base64.txt img3.jpg img3_base64.txt
Para testar nossa função getBase64Encoding (), execute o código abaixo:
func TestGetBase64Encoding (t * testing.T) {cases:=[] string {“img1″,”img2″,”img3”} para _, v:=range cases {t.Run (v, func (t * testing.T) {b, err:=os.ReadFile (filepath.Join (“testdata”, v +”.jpg”)) if err!=nil {t.Fatal (err) } esperado, errar:=os.ReadFile (filepath.Join (“testdata”, v +”_ base64.txt”)) se errar!=nil {t.Fatal (err)} obtido:=getBase64Encoding (b) se string (esperado )!=got {t.Fatalf (“Saída esperada para ser:’% s’, mas obteve:’% s’”, string (esperado), obtido}})}}
Os bytes para cada arquivo de amostra são lidos do sistema de arquivos e, em seguida, alimentados na função getBase64Encoding (). A saída é subsequentemente comparada à saída esperada, que também é recuperada do diretório testdata.
Vamos tornar o teste mais fácil de manter criando um subdiretório dentro de testdata. Dentro de nosso subdiretório, adicionaremos todos os arquivos de entrada, permitindo-nos simplesmente iterar sobre cada arquivo binário e comparar a saída real com a esperada.
Agora, podemos adicionar mais casos de teste sem tocar o código-fonte:
$ go test-v===RUN TestGetBase64Encoding===RUN TestGetBase64Encoding/img1===RUN TestGetBase64Encoding/img2===RUN TestGetBase64Encoding/img3—PASS: TestGetBase64Encoding (0,04s)–PASS: TestGetBase64Encoding/img1 (0.01s)—PASS: TestGetBase64Encoding/img2 (0.01s)—PASS: TestGetBase64Encoding/img3 (0.01s) PASS ok github.com/ayoisaiah/random 0.044s
Usando arquivos dourados
Se você estiver usando um modelo Go, é uma boa ideia testar a saída gerada contra a saída esperada para confirmar se o modelo está funcionando conforme o planejado. Os modelos Go geralmente são grandes, por isso não é recomendado codificar permanentemente a saída esperada no código-fonte, como fizemos até agora neste tutorial.
Vamos explorar uma abordagem alternativa para os modelos Go que simplifica a escrita e manter um teste ao longo do ciclo de vida de um projeto.
Um arquivo dourado é um tipo especial de arquivo que contém a saída esperada de um teste. A função de teste lê o arquivo dourado, comparando seu conteúdo com a saída esperada de um teste.
No exemplo a seguir, usaremos um html/template para gerar uma tabela HTML que contém uma linha para cada livro em um inventário:
digite Book struct {Name string Author string Publisher string Pages int PublishedYear int Price int} var tmpl=`
Nome | Autor | Editora | Páginas | Ano | Preço |
---|---|---|---|---|---|
{{.Name}} | {{.Author}} | {{.Publisher}} | {{. Pages}} | {{.PublishedYear}} | $ {{.Price}} |
`var tpl=template.Must (template.New (“table”). Parse (tmpl)) func generateTable (books [] Book, w io.Writer) error {return tpl.Execute (w, books)} func main () {livros:=[] Livro {{Nome:”The Odessa File”, Autor:”Frederick Forsyth”, Pages: 334, PublishedYear: 1979, Editora:”Bantam”, Preço: 15,},} err:=generateTable (books, os.Stdout) if err!=nil {log.Fatal (err)}}
A função generateTable () acima cria a tabela HTML a partir de uma parte dos objetos Book. O código acima produzirá a seguinte saída:
$ go run main.go
Nome | Autor | Editora | Pages | Ano | Preço |
---|---|---|---|---|---|
The Odessa Arquivo | Frederick Forsyth | Bantam | 334 | 1979 | $ 15 |
Para testar a função acima, capturaremos o resultado real e o compararemos com o resultado esperado. Armazenaremos o resultado esperado no diretório testdata como fizemos na seção anterior, no entanto, teremos que fazer algumas alterações.
Suponha que temos a seguinte lista de livros em um inventário:
var inventário=[] Livro {{Nome:”The Solitare Mystery”, Autor:”Jostein Gaarder”, Editora:”Farrar Straus Giroux”, Pages: 351, Publicado Ano: 1990, Preço: 12,}, { Nome:”Também conhecido como”, Autor:”Robin Benway”, Editora:”Walker Books”, Páginas: 208, Publicado no ano: 2013, Preço: 10,}, {Nome:”Ego Is the Enemy”, Autor:”Ryan Holiday”, Editora:”Portfólio”, Pages: 226, Publicado Ano: 2016, Preço: 18,},}
A produção esperada para esta lista de livros se estenderá por muitas linhas, portanto, é difícil t o coloque-o como uma string literal dentro do código-fonte:
Nome | Autor | Editor | Pages | Ano | Preço |
---|---|---|---|---|---|
O mistério do Solitaire | Jostein Gaarder | Farrar Straus Giroux | 351 | 1990 | $ 12 |
Também conhecido como | Walker Books | 308 | 2013 | US $ 10 | |
Ego é o inimigo | Ryan Holiday | Portfólio | 226 | 2016 | $ 18 |
Além de ser prático para saídas maiores, um arquivo dourado pode ser atualizado e gerado automaticamente.
Embora seja possível escrever uma função auxiliar para criar e atualizar arquivos dourados, podemos aproveite as vantagens do goldie , um utilitário criado especificamente para arquivos dourados.
Instale a versão mais recente do goldie com o comando abaixo:
$ go get-u github.com/sebdah/goldie/v2
Vamos usar goldie em um teste para a função generateTable ():
func TestGenerateTable (t * testing.T) {var buf bytes.Buffer err:=generateTable (inventory, & buf) if err!=nil {t.Fatal (err)} real:=buf.Bytes () g:=goldie.New (t) g.Assert (t,”books”, real)}
O teste acima captura a saída da função generateTable () em um buffer de bytes. Em seguida, ele passa o conteúdo do buffer para o método Assert () na instância goldie. O conteúdo do buffer será comparado ao conteúdo do arquivo books.golden no diretório testdata.
Inicialmente, a execução do teste falhará porque ainda não criamos o arquivo books.golden:
$ go test-v===RUN TestGenerateTable main_test.go: 48: Fixação dourada não encontrada. Tente executar com o sinalizador-update.—FALHA: TestGenerateTable (0,00s) FALHA status de saída 1 FALHA github.com/ayoisaiah/random 0,006s
A mensagem de erro sugere que adicionemos o sinalizador-update, que criará o arquivo books.golden com o conteúdo do buffer:
$ go test-v-update===RUN TestGenerateTable—PASS: TestGenerateTable (0,00s) PASS ok github.com/ayoisaiah/random 0,006s
Nas execuções subsequentes, devemos remova o sinalizador-update para que nosso arquivo dourado não seja continuamente atualizado.
Qualquer mudança no modelo deve causar a falha do teste. Por exemplo, se você atualizar o campo de preço para euros em vez de dólares americanos, receberá imediatamente um erro. Esses erros ocorrem porque a saída da função generateTable () não corresponde mais ao conteúdo do arquivo dourado.
Goldie fornece recursos de comparação para ajudá-lo a identificar a mudança quando esses erros ocorrem:
$ go test-v===RUN TestGenerateTable main_test.go: 48: O resultado não corresponde ao acessório dourado. A diferença está abaixo:—Esperado +++ Real @@-18,3 +18,3 @@
+
–
+
–
+
—FALHA: TestGenerateTable (0,00s) FALHA status de saída 1 FALHA github.com/ayoisaiah/random 0,007s
Na saída acima, a alteração está claramente destacada. Essas alterações são deliberadas, então podemos fazer nosso teste passar novamente atualizando o arquivo dourado usando a sinalização-update:
$ go test-v-update===RUN TestGenerateTable—PASS: TestGenerateTable (0,00s) PASS ok github.com/ayoisaiah/random 0,006s
Conclusão
Neste tutorial, vimos algumas técnicas de teste avançadas no Go. Primeiro, examinamos nossos pacotes HTTP em profundidade e aprendemos como simular nosso cliente HTTP com uma interface personalizada. Em seguida, revisamos como usar dados externos em testes e criar arquivos dourados usando goldie.
Espero que você tenha achado este post útil. Se você deseja compartilhar alguma técnica adicional, deixe um comentário abaixo. Obrigado pela leitura e feliz codificação!