Nos testes de unidade, os desenvolvedores testam funções, métodos, módulos e pacotes individuais para verificar se estão corretos. O teste de unidade ajuda a encontrar e corrigir bugs no início do ciclo de desenvolvimento e evita regressões durante a refatoração. Um bom teste de unidade também pode servir como uma forma de documentação para desenvolvedores que são novos no projeto.

Neste tutorial, vamos cobrir como escrever testes de unidade em Go usando o pacote de teste integrado e várias ferramentas externas. Ao final deste artigo, você entenderá conceitos como testes baseados em tabelas, injeção de dependência e cobertura de código.

Vamos começar!

Escrevendo seu primeiro teste em Go

Para entender os testes em Go, escreveremos um programa básico que calcula o produto de dois inteiros. Em seguida, escreveremos um teste que verifica a exatidão de sua saída.

Primeiro, crie um diretório em seu sistema de arquivos e navegue até ele. Na raiz do diretório, crie um arquivo chamado integers.go e adicione o seguinte código:

//integers.go package main import (“fmt”)//Multiply retorna o produto de dois inteiros func Multiply (a, b int) int {return a * b}

Vamos escrever um teste para verificar se a função Multiply () funciona corretamente. No diretório atual, crie um arquivo chamado integers_test.go e adicione o seguinte código a ele:

Anatomia de um teste Go

A convenção de nomenclatura testar arquivos em Go é terminar o nome do arquivo com o sufixo _test.go e colocar o arquivo no mesmo diretório do código que ele testa. No exemplo acima, a função Multiply está em integers.go, então seus testes são colocados em integers_test.go. Observe que Go não envia arquivos de teste em quaisquer binários que ele produz, porque eles não são necessários para a execução do código. Em Go, uma função de teste deve sempre usar a seguinte assinatura:

func TestXxx (* testing.T)

O nome de um teste começa com o prefixo Teste, seguido pelo nome da função sendo testada, Xxx. Leva um único argumento, que é um ponteiro do tipo testing.T. O tipo exporta vários métodos para tarefas como relatar erros, registrar valores intermediários e especificar métodos auxiliares.

Em nosso exemplo na seção anterior, a variável obtida dentro da função TestMultiply () é atribuída ao resultado de a chamada de função Multiply (2, 3). want é atribuído ao resultado esperado 6.

A última parte do teste verifica se os valores de want e got são iguais. Caso contrário, o método Errorf () é invocado, falhando no teste.

Executando testes Go

Agora, vamos usar o comando go test para executar nosso teste no terminal. Enquanto o Go está instalado, o comando go test já está disponível em sua máquina.

O comando go test compila as fontes, arquivos e testes encontrados no diretório atual e, em seguida, executa o binário de teste resultante. Quando o teste estiver concluído, um resumo do teste, PASSA ou FALHA, será impresso no console, como visto no bloco de código abaixo:

$ go test PASS ok github.com/ayoisaiah/random 0,003s

Quando você usa go test como acima, o cache é desabilitado, então os testes são executados todas as vezes.
Você também pode optar pelo modo de lista de pacotes usando go test., que armazena em cache resultados de teste bem-sucedidos e evita reexecuções desnecessárias.

Você pode executar testes em um pacote específico passando o caminho relativo para o pacote, por exemplo, go test./package-name. Além disso, você pode usar go test./… para executar os testes de todos os pacotes na base de código:

$ go test. ok github.com/ayoisaiah/random (cached)

Se você anexar o sinalizador-v para fazer o teste, o teste imprimirá os nomes de todas as funções de teste executadas e o tempo gasto para sua execução. Além disso, o teste exibe a saída da impressão no log de erros, por exemplo, quando você usa t.Log () ou t.Logf ():

$ go test-v===RUN TestMultiply—PASS: TestMultiply (0,00s) PASSOU ok github.com/ayoisaiah/random 0,002s

Vamos fazer com que nosso teste falhe alterando querer 7. Execute go test mais uma vez e inspecione sua saída:

$ go test-v—FALHA: TestMultiply (0,00s) integers_test.go: 10: Esperado’7′, mas obteve’6’FALHA status de saída 1 FALHA github.com/ayoisaiah/random 0,003s

Como você pode ver, o teste falhou e a mensagem passada para a função t.Errorf () está presente na mensagem de falha. Se você retornar o valor desejado para 6, o teste passará novamente.

Testes baseados em tabela no Go

O exemplo de teste acima contém apenas um único caso. No entanto, qualquer teste razoavelmente abrangente teria vários casos de teste, garantindo que cada unidade de código fosse suficientemente auditada em relação a uma gama de valores.

Em Go, usamos testes orientados a tabelas, que nos permitem definir todos os nossos testa casos em uma fatia, itera sobre eles e executa comparações para determinar se o caso de teste foi bem-sucedido ou falhou:

digite testCase struct {arg1 int arg2 int want int} func TestMultiply (t * testing.T) {cases:=[] testCase {{2, 3, 6}, {10, 5, 50}, {-8,-3, 24}, {0, 9, 0}, {-7, 6,-42},} para _, tc:=casos de intervalo {obtido:=Multiply (tc.arg1, tc.arg2) if tc.want!=obtido {t.Errorf (“Esperado’% d’, mas obtido’% d'”, tc.want, got)}}}

No trecho de código acima, usamos a estrutura testCase para definir as entradas para cada caso de teste.
As propriedades arg1 e arg2 representam os argumentos para Multiply, enquanto want é o resultado esperado para o caso de teste.

A fatia de casos é usada para configurar todos os casos de teste para a função Multiply. Observe que os nomes das propriedades são omitidos para simplificar.

Para testar cada caso, precisamos iterar sobre a fatia de casos, passar arg1 e arg2 de cada caso para Multiply () e, em seguida, confirmar se o valor de retorno é igual ao que deseja especificado. Podemos testar quantos casos forem necessários usando esta configuração.

Se você executar o teste novamente, ele será aprovado:

$ go test-v===RUN TestMultiply—PASS: TestMultiply (0.00s) PASS ok github.com/ayoisaiah/random 0.002s

Falha no teste de sinalização

Nos exemplos acima, usamos o método t.Errorf () para falhar nos testes. Usar t.Errorf () é equivalente a invocar t.Logf (), que registra texto no console em falhas de teste ou quando o sinalizador-v é fornecido, seguido por t.Fail (), que marca a função atual como falha sem interromper sua execução.

Usar t.Errorf () evita uma falha de teste quando interrompemos a função, permitindo-nos reunir mais informações para corrigir o problema. Além disso, em um teste conduzido por tabela, t.Errorf () nos permite falhar em um caso específico sem afetar a execução de outros testes.

Se uma função de teste não puder se recuperar de uma falha, você pode interrompê-la imediatamente invocando t.Fatal () ou t.Fatalf (). Qualquer um dos métodos marca a função atual como falha, interrompendo sua execução imediatamente. Esses métodos são equivalentes a chamar t.Log () ou t.Logf (), seguido por t.FailNow ().

Usando subtestes

Usar um teste baseado em tabela é eficaz, no entanto, há uma grande falha-a incapacidade de executar seletivamente um caso de teste individual sem executar todos os casos de teste.

Uma solução para este problema é comentar todos os casos de teste que são irrelevantes no momento e descomente-os novamente mais tarde. No entanto, fazer isso é tedioso e sujeito a erros. Para este cenário, usaremos um subteste!

No Go 1.7, podemos dividir cada caso de teste em um teste exclusivo que é executado em uma goroutine separada adicionando um método Run () ao teste. Tipo T. O método Run () leva o nome do subteste como seu primeiro argumento e a função de subteste como o segundo. Você pode usar o nome do teste para identificar e executar o subteste individualmente.

Para vê-lo em ação, vamos atualizar nosso teste TestMultiply, conforme mostrado abaixo:

func TestMultiply (t * testing.T) {casos:=[] testCase {{2, 3, 6}, {10, 5, 50}, {-8,-3, 24}, {0, 9, 0}, {-7, 6,-42 },} para _, tc:=casos de intervalo {t.Run (fmt.Sprintf (“% d *% d=% d”, tc.arg1, tc.arg2, tc.want), func (teste t *. T) {obteve:=Multiplicar (tc.arg1, tc.arg2) if tc.want!=Obteve {t.Errorf (“Esperado’% d’, mas obteve’% d'”, tc.want, obteve)} })}}

Agora, quando você executa os testes com o sinalizador-v, cada caso de teste individual será relatado na saída. Como construímos o nome de cada teste a partir dos valores em cada caso de teste, é fácil identificar um caso de teste específico que falhou.

Para nomear nossos casos de teste, adicionaremos uma propriedade de nome ao testCase struct. É importante notar que a função TestMultiply não termina de ser executada até que todos os seus subtestes tenham saído:

$ go test-v===RUN TestMultiply===RUN TestMultiply/2 * 3=6===RUN TestMultiply/10 * 5=50===EXECUTAR TestMultiply/-8 *-3=24===EXECUTAR TestMultiply/0 * 9=0===EXECUTAR TestMultiply/-7 * 6=-42—APROVADO: TestMultiply (0,00s )—APROVADO: TestMultiply/2 * 3=6 (0,00s)—APROVADO: TestMultiply/10 * 5=50 (0,00s)—APROVADO: TestMultiply/-8 *-3=24 (0,00s) )—PASSAR: TestMultiply/0 * 9=0 (0,00s)—PASSAR: TestMultiply/-7 * 6=-42 (0,00s) PASSAR ok github.com/ayoisaiah/random 0,003s

Medição cobertura de código

A cobertura de código conta as linhas de código que são executadas com sucesso quando o seu conjunto de testes está em execução, representando a porcentagem do seu código coberto pelo seu conjunto de testes. Por exemplo, se você tem uma cobertura de código de 80 por cento, significa que 20 por cento da base de código está sem testes.

Método de cobertura de código integrado do Go

Go fornece um método integrado de cobertura de código-in método para verificar sua cobertura de código. Desde Go v1.2, os desenvolvedores podem usar a opção-cover com go test para gerar um relatório de cobertura de código:

$ go test-cover Cobertura PASSA: 100,0% das declarações ok github.com/ayoisaiah/random 0,002s

Conseguimos atingir 100 por cento de cobertura de teste para nosso código, no entanto, testamos apenas uma única função de forma abrangente. Vamos adicionar uma nova função no arquivo integers.go sem escrever um teste para ela:

//integers.go//Add retorna a soma de dois inteiros func Add (a, b int) int {return a + b }

Quando executarmos os testes novamente com a opção-cover, veremos uma cobertura de apenas 50 por cento:

$ go test-cover Cobertura PASSA: 50,0% das declarações ok github.com/ayoisaiah/random 0.002s

Examinando nossa base de código

Embora saibamos qual porcentagem de nossa base de código é coberta, não sabemos quais partes de nossa base de código não estão cobertas. Vamos converter o relatório de cobertura em um arquivo usando a opção–coverprofile para que possamos examiná-lo mais de perto:

$ go test-coverprofile=cobertura.out PASSAR cobertura: 50,0% das declarações ok github.com/ayoisaiah/random 0,002s

No bloco de código acima, os testes são executados como antes, e a cobertura do código é impressa no console.
No entanto, os resultados do teste também são salvos em um novo arquivo chamado cobertura.out no trabalho atual. diretório. Para estudar esses resultados, vamos executar o seguinte comando, que divide o relatório de cobertura por função:

$ go tool cover-func=verification.out github.com/ayoisaiah/random/integers.go:4: Multiply 100.0 % github.com/ayoisaiah/random/integers.go:9: Adicionar 0,0% total: (declarações) 50,0%

O bloco de código acima mostra que a função Multiply () está totalmente coberta, enquanto a função Add () tem apenas 50 por cento de cobertura geral.

Método de cobertura HTML

Outra maneira de ver os resultados é por meio de uma representação HTML. O bloco de código abaixo abrirá o navegador padrão automaticamente, mostrando as linhas cobertas em verde, as linhas não cobertas em vermelho e as declarações não contadas em cinza:

$ go tool cover-html=Cover.out

Usando o HTML O método de cobertura facilita a visualização do que você ainda não cobriu. Se o pacote que está sendo testado tiver vários arquivos, você pode selecionar cada arquivo da entrada no canto superior direito para ver o detalhamento da cobertura:

Vamos obter a cobertura de código de volta para 100 por cento em adicionar um teste para a função Add (), conforme mostrado abaixo:

func TestAdd (t * testing.T) {cases:=[] test {{1, 1, 2}, {7, 5, 12} , {-19,-3,-22}, {-1, 8, 7}, {-12, 0,-12},} para _, tc:=casos de intervalo {obtido:=Adicionar (tc.arg1, tc.arg2) if tc.want!=got {t.Errorf (“Esperado’% d’, mas obtido’% d'”, tc.want, got)}}}

Executar os testes novamente deve exibir um cobertura de código de 100 por cento:

$ go test-cover cobertura PASS: 100,0% das declarações ok github.com/ayoisaiah/random/integers 0,003s

Executando um teste específico

Digamos que que você tem muitos arquivos de teste e funções, mas você deseja isolar apenas um ou alguns para executar. Podemos fazer isso usando a opção-run. Por exemplo, se quisermos executar apenas os testes para a função Adicionar, passaremos o nome da função de teste como um argumento para-run:

$ go test-v-run=TestAdd===RUN TestAdd–PASS: TestAdd (0,00s) PASS ok github.com/ayoisaiah/random/integers 0,003s

Como você pode ver na saída acima, apenas o método TestAdd foi executado. Observe que o argumento para-run é interpretado como uma expressão regular, então todos os testes que correspondem ao regex fornecido serão executados.

Se você tiver um conjunto de funções de teste que começam com o mesmo prefixo, como TestAdd_NegativeNumbers e TestAdd_PositiveNumbers, você pode executá-los isoladamente passando o prefixo, TestAdd, para-run.

Agora, vamos supor que queremos apenas executar TestAdd e TestMultiply, mas temos outras funções de teste. Podemos usar uma barra vertical para separar seus nomes no argumento para-run:

$ go test-v-run=’TestAdd | TestMultiply’===RUN TestMultiply—PASS: TestMultiply (0,00s)===RUN TestAdd—PASS: TestAdd (0,00s) PASS ok github.com/ayoisaiah/random/integers 0,002s

Você também pode executar um subteste específico passando seu nome para-run. Por exemplo, podemos executar qualquer um dos subtestes na função TestMultiply (), conforme mostrado abaixo:

$ go test-v-run=’TestMultiply/2 * 3=6’===RUN TestMultiply===EXECUTAR TestMultiply/2 * 3=6—PASSAR: TestMultiply (0,00s)—PASSAR: TestMultiply/2 * 3=6 (0,00s) PASSAR ok github.com/ayoisaiah/random 0,003s

Injeção de dependência

Vamos supor que temos uma função que imprime alguma saída no console, como mostrado abaixo:

//printer.go func Print (text string) {fmt.Println (text)}

A função Print () acima mostra seu argumento string para o console. Para testá-lo, temos que capturar sua saída e compará-la com o valor esperado. No entanto, como não temos controle sobre a implementação de fmt.Println (), usar esse método não funcionará em nosso caso. Em vez disso, podemos refatorar a função Print (), tornando mais fácil capturar sua saída.

Primeiro, vamos substituir a chamada para Println () por uma chamada para Fprintln (), que leva um io.Writer interface como seu primeiro argumento, especificando onde sua saída deve ser escrita. Em nosso exemplo abaixo, este local é especificado como os.Stdout. Agora, podemos corresponder ao comportamento fornecido por Println:

func Print (text string) {fmt.Fprintln (os.Stdout, text)}

Para nossa função, não importa onde imprimimos o texto. Portanto, em vez de codificar os.Stdout, devemos aceitar uma interface io.Writer e passá-la para fmt.Fprintln:

func Print (string de texto, w io.Writer) {fmt.Fprintln (w, text )}

Agora, podemos controlar onde a saída da função Print () é escrita, tornando mais fácil testar nossa função. No teste de exemplo abaixo, usaremos um buffer de bytes para capturar a saída de Print () e, em seguida, compará-lo ao resultado esperado:

//printer_test.go func TestPrint (t * testing.T) { var buf bytes.Buffer text:=”Olá, mundo!”Print (text, & buf) got:=strings.TrimSpace (buf.String ()) if got!=Text {t.Errorf (“Saída esperada para ser:% s, mas obteve:% s”, text, got)} }

Ao utilizar Print () em seu código-fonte, você pode facilmente injetar um tipo concreto e escrever na saída padrão:

func main () {Print (“Hello, World!”, os.Stdout) }

Embora o exemplo acima seja bastante trivial, ele ilustra um método para passar de uma função especializada para uma de propósito geral, permitindo a injeção de dependências diferentes.

Conclusão

Escrever testes de unidade garante que cada unidade de código está funcionando corretamente, aumentando a chance de que seu aplicativo como um todo funcione conforme planejado.

Ter testes de unidade adequados também é útil ao refatorar, ajudando a prevenir regressões. O pacote de teste integrado e o comando go test fornecem recursos consideráveis ​​de teste de unidade. Você pode aprender mais consultando a documentação oficial .

Obrigado pela leitura e feliz codificação!