Go Test-Drive: Automatizando Testes e Relatórios de Cobertura
Olá leitor, seja muito bem vindo a mais uma lição da nossa jornada de GoLang no Portal da Micilini 😊
Se você caiu aqui de paraquedas, saiba que nós temos diversos conteúdos que tratam sobre GoLang aqui neste portal, para acessá-los para seguir este link.

Nesta aula, vamos entender por que os testes automatizados são tão importantes em qualquer projeto em Go, e como estruturar seu código para tirar o máximo proveito deles.
Ao final desta lição, esperamos que você não tenha apenas o conhecimento teórico, mas também exemplos de testes práticos para rodar no seu ambiente em Go, além é claro, de garantir que o seu código permaneça confiável à medida que ele evolui 😎
Vamos nessa?
Por que a sua aplicação precisa de testes?
Imagine que você tem um serviço Go que calcula descontos para clientes. Tudo funciona perfeitamente em desenvolvimento, mas no primeiro dia de produção, um bug faz o sistema conceder 100% de desconto a todos.
Centenas de pedidos são processados de graça, impactando diretamente o faturamento da empresa — e tudo isso por causa de uma mudança aparentemente inócua numa função de cálculo que ninguém validou.
Se tivéssemos um conjunto de testes automatizados cobrindo as regras de negócio de desconto, esse erro teria sido detectado antes de chegar ao cliente.
Gostaria que neste primeiro momento, você imaginasse que esses testes fossem como um seguro contra prejuízos que podem acontecer no seu próprio código.
Se os testes tivessem sido implementados na aplicação de pedidos e descontos, tais erros nunca teriam acontecido, e a empresa não teria maiores dores de cabeça em dar explicações plausíveis para os investidores e clientes.
Mas será que em casos como esses, a empresa tem todo o direito de reaver e cancelar todas as compras? E o que o direito do consumidor fala sobre isso? Será que por causa desse erro, a partir de agora a empresa é obrigada a entregar seus produtos e assumir o prejuízo?
O fato é que nós não estamos aqui para discutir isso, mas sim, evitar ou ignorar a implementação de testes automatizados, podem deixar passar diversos problemas no seu código, gerando prejuízos a sua empresa.
Mas afinal, você sabe o que é um teste automatizado?
O que são testes automatizados?
Por definição, testes automatizados são scripts que executam partes do seu código de forma separada e automática, com o intuito analisar se os resultados gerados estão de acordo com os esperados.
É como se você tivesse instalado diversas fechaduras novas na sua casa, e tivesse que testar cada um delas para garantir que a sua porta está sendo fechada de forma segura. Mas em vez de você mesmo fazer isso (manualmente), você contrata um robô para fazer isso por você.
E no final o robô contratado pode te trazer um resultados similar a este:
Número de Fechaduras testadas: 12
Quantidade de Fechaduras Funcionais: 10
Quantidade de Fechaduras com Problemas: 2
Quais fechaduras deram problemas: A do quarto do segundo andar, e da porta dos fundos da casa.
No universo do GoLang, a ideia é que você escreva um teste apenas uma única vez, e depois disso, executar um comando como go test
, a fim de rodar todos os seus testes sem nenhum tipo de intervenção manual.
Quais os benefícios de se implementar testes automatizados?
Além dos possíveis benefícios implícitos que foram mencionados acima, no mundo da tecnologia, podemos citar três benefícios principais:
1) Detecção precoce de regressões: quando temos mudanças acidentais no código fonte da aplicação, tais testes costumam sinalizar imediatamente que algo não aconteceu como o esperado. O que evita com que você suba códigos com problemas para o seu ambiente de produção.
2) Testes automatizados são representações de documentação viva: tais testes mostram na prática como a sua API ou funções devem se comportar.
Mas é claro que isso não significa que devemos abandonar a criação de documentações oficiais da nossa aplicação, muito pelo contrário, elas são muito importantes e relevantes a todos os desenvolvedores da equipe e não devem ser substituídas por testes automatizados.
3) Testes automatizados te dão mais confiança para refatorar seu código: quando implementamos estes testes, adquirimos muito mais confiança para reorganizar nosso código sem medo de quebrar funcionalidades existentes.
Quais são os tipos de testes automatizados existentes hoje no mercado?
Se tratando de testes automatizados, hoje nos contamos com 3 categorias principais em que costumam-se agrupar a maior parte desses tipos de testes, apesar do universo de testes ser mais amplo.
Veremos abaixo, todos os tipos de testes que temos disponíveis no mercado hoje!
Testes Unitários

Começando com os testes unitários, eles representam o alicerce da pirâmide de testes, onde são considerados os tipos de testes mais comum encontrados em projetos mundo a fora.
Além disso, testes unitários costumam ser os primeiros testes que são criados dentro de um projeto, mas o que vem a ser um teste unitário? 🧐
Pense em um grande projeto que conta com diversas funcionalidades diferentes e chamadas de API (endpoints). Agora imagine que você pegou uma pequena parte desse sistema e gostaria de testar apenas uma funcionalidade. Isso é um teste unitário.
Em outras palavras, estes tipos de testes testam apenas uma única função ou método, isolando-o de qualquer dependência externa (banco, rede, sistema de arquivos, outros serviços).
Com isso, esses testes garantem que cada pedaço mínimo de lógica, funcione conforme o esperado em todos os cenários previstos (como valores de entrada “normais”, limites, casos de erro etc).
Supondo que você tem um e-commerce com centenas de endpoints e funcionalidades, e que gostaria de focar apenas em testar a lógica que aplica um cupom de desconto. Vejamos abaixo o uso de 2 exemplos usando testes unitários neste cenário:
Cenário 1) Testando a sua função de descontos.
Um dos primeiros lugares que você pode começar realizando testes unitários é pela função que aplica cupons de desconto.
Imagine que nossa função de descontos conta com a seguinte sintaxe:
ApplyCoupon(code string, cartTotal float64)
Como podemos notar, ela recebe o código do cupom e o valor total do carrinho.
No caso mais comum, um cupom de 10% deve reduzir um carrinho de R$ 200,00 para R$ 180,00, enquanto há cupons especiais que podem chegar a 100% de desconto — transformando R$ 50,00 em zero.
Mas, assim que você passa um código inválido ou expirado, a função precisa detectar o erro e devolver o valor original do carrinho, junto de um erro, como por exemplo: ErrCouponInvalid
.
Um teste unitário bem escrito para esse cenário vai simular apenas o repositório de cupons (por meio de um stub ou mock que retorna validade e existência), de forma que não haja acesso ao banco de dados real nem a nenhuma chamada externa, com o objetivo de verificar esses três cenários: o desconto “normal”, o desconto total e a exceção em caso de algum erro.
No caso do exemplo acima, a ideia é criar uma outra função que seja executada de forma separada do nosso sistema principal, mas que seja capaz de enviar dados para a função ApplyCoupon()
, de modo a validar se os resultados advindos dessa função estão corretos ou não. Isso é um teste unitário.
No final das contas, é apenas uma função separada em um arquivo diferente que tem o intuito de testar uma função usada pelo sistema.
Cenário 2) Verificação de estoque antes de finalizar pedido
Supondo que o nosso sistema conte com uma segunda função que é responsável por verificar se o produto existe no estoque antes de levar o usuário para a tela de pagamento:
CheckStock(sku string, quantity int) error
Num cenário de estoque suficiente, se o stub do repositório indica cinco unidades disponíveis e você pede três, a função não deverá retornar nenhuma mensagem de erro, pois existem produtos no estoque.
Além disso, se você pedir exatamente cinco produtos, é esperado que a função seja executada tranquilamente, isto é, sem retornar erro algum.
Entretanto, se a quantidade solicitada for seis unidades quando só existem apenas cinco no estoque, a função precisa retornar ErrOutOfStock
. E se não retornar... significa que seu código possui FALHAS!
Sendo assim, a ideia seria construir mais outra função separada do sistema principal que fosse capaz de testar nossa função CheckStock()
, de modo a validar como o sistema se comporta quando tentamos fechar uma compra de um produto que está fora do estoque. Isso é um teste unitário!
Vejamos agora algumas das características principais de um teste unitário:
- Rapidez extrema: executam em milissegundos, permitindo rodar centenas ou milhares a cada compilação.
- Determinísticos: não dependem de estado externo; dado o mesmo input, sempre obtém o mesmo output.
- Fáceis de escrever e manter: como eles são curtos e focados, tendem a mudar pouca coisa durante a evolução do nosso código.
Quando devemos fazer o uso de testes unitários?
- Adicionou uma nova função, ou uma nova lógica de negócios? Então é bom você considerar a implementação de um teste unitário.
Testes de Integração

Imagine que a sua aplicação possuí módulos que se comunicam entre si, e você quer testar se a integração entre esses dois componentes está correta. Isso é um teste de integração!
O objetivo desses tipos de testes é verificar se as camadas "se casam" corretamente entre si. Como é o caso de queries SQL, serialização de JSON, mapeamentos de objetos, contratos de APIs internos etc.
Vejamos alguns exemplos reais de testes de integração:
- Testar se um sistema de criação de pedidos (
/api/orders
) está recebendo corretamente um payload com os itens e quantidades. Onde por de baixo dos panos, o sistema está alterando de forma correta os dados em nossa base de dados, e retornando a resposta esperada em formato JSON de volta ao cliente. - Testar se a consulta de produtos (
/api/products/{id}
) está disparando corretamente uma requisição de modo a retornar os campos básicos de um produto junto com todos os seus atributos extras, incluindo lógicas capazes de validar promoções e preços promocionais.
No caso dos exemplos acima, cada um dos teste de integração valida não só o endpoint, mas também a persistência, mapeamentos e fluxo transacional que está acontecendo entre módulos. Ou seja, é muito mais do que apenas um único teste unitário 😉
Vejamos agora algumas das características principais de um teste de integração:
- Mais lentos se comparados aos testes unitários: podem exigir instância de banco (SQLite em memória, Docker), fila de mensagens, ou mocks sofisticados.
- Mais complexos de isolar: como o próprio nome já diz, testes integrados! Às vezes alguns desses testes fazem o uso de bancos de testes ou versões stub de serviços externos, o que por si só, aumenta a complexidade.
- Mais “realistas” que unitários: em vez de testar apenas uma única unidade, eles pegam mais do fluxo real do trafego de dados na sua aplicação.
Quando devemos fazer o uso de testes de integração?
- Em um cenário utópico, só implemente teste de integração depois de você ter uma cobertura sólida de testes unitários implementados, isto para garantir que os componentes acoplados funcionem de forma conjunta.
- Além disso, o uso desses tipos de testes são excelentes para validar migrações de esquema, scripts de inicialização ou formatos de dados compartilhados.
Testes de Ponta a Ponta (E2E)

Como o próprio nome já nos diz, testes de ponta a ponta (E2E) cobrem o sistema como um todo, desde o usuário (ou cliente HTTP) até a camada de dados.
Eles exercem fluxos completos, onde normalmente usamos o método HTTP como via de entrada, interações com a UI, ou até APIs externas.
O objetivo desses testes é garantir que todo o sistema esteja configurado corretamente, desde a parte do roteamento, ligações com middlewares, uso da rede, esquema de autenticação do usuário, interações com a UI e até mesmo interação real.
Vejamos algumas das características de um teste ponta a ponta:
- Costumam ser mais lentos e frágeis: onde qualquer mudança de rota, layout ou tempo de resposta pode quebrá-los facilmente, pois há muitos módulos/pacotes envolvidos.
- Manutenção mais custosa: uma vez que requerem ambiente de testes mais robustos, como: containers, mock servers, navegadores headless.
- Te dá uma maior confiança para deploy: esses tipos de testes costumam ser a última barreira antes de entregar ao usuário final, pois os mesmos simulam cenários de utilização reais.
Quando devemos fazer o uso de teste de ponta a ponta?
- Em ciclos de release (pré-produção), para validar flows críticos: login, pagamento, upload de arquivos, etc.
- Como “cheque final” no pipeline CI/CD, especialmente em merges para branches estáveis.
Outros tipos de testes automatizados que você pode aplicar em seus projetos
Não é só de testes unitários, integrados e de ponto a ponta que um projeto sobrevive, você sabia?
Além deles, existem mais 5 tipos de testes que podemos implementar em nossas aplicações, são eles:
Smoke tests: um subconjunto muito pequeno de E2E, apenas para verificar que o sistema levanta sem crash e endpoints principais estão vivos.
Testes de contrato (contract tests): garantem que duas equipes (cliente vs servidor) respeitam o acordo de API.
Testes de performance e carga: medem throughput, latência e escalabilidade sob uso intensivo.
Testes de segurança: verificam vulnerabilidades comuns (injeção, XSS, CSRF).
Testes de fuzzing: injetam inputs aleatórios para descobrir falhas não previstas.
Mas não se assuste, a base começa mesmo pelos unitários, depois integração e, somente quando fizer sentido de investimento em infraestrutura de testes, parta para E2E e as especializações que citamos acima.
Agora chega de papo e vamos criar o nosso projeto de testes 😁
Criando seu projeto de testes
Dentro da pasta JornadaGoLang, nós iremos criar uma nova pasta chamada de 27-testes-em-go
, onde dentro dela, vamos criar o nosso arquivo main.go
:
package main
func main(){
}
Feito isso, partiu criar testes com GoLang 😋
Criando testes unitários básicos
Nada melhor como começar do básico do básico, não é mesmo?
Neste momento, nós iremos criar um pequeno exercício de testes unitários onde ele será aplicado dentro de uma função criada para calcular métricas.
Para isso dentro da pasta do seu projeto, vamos criar uma nova subpasta chamada de math, onde dentro dela criaremos um novo arquivo chamado de math.go:

A ideia é criamos um novo módulo capaz de conter diversas funções de cálculos, para que posteriormente, possamos criar testes unitários para cada uma dessas funções, de modo a validar se os resultados gerados por elas, são os esperados ou não.
Inicialmente vamos criar este arquivo apenas com uma única função chamada de DiscoverMedia
, que é usada para descobrir a média entre 2 ou mais números:
package math
import (
"fmt"
"strconv"
)
// Calcula a média entre dois ou mais valores recebidos por parâmetros
func DiscoverMedia(numbers ...float64) float64 {
total := 0.0
for _, num := range numbers {
total += num
}
media := total / float64(len(numbers))
// formata com duas casas decimais
result, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", media), 64)
return result
}
Feito isso, é importante que você saiba que, por convenção, os arquivos em GoLang que realizam testes automatizados usam a nomenclatura _test.go
sempre no final do nome do arquivo.
Sendo assim, como queremos criar testes para o arquivo math.go
, o ideal é que criemos um novo arquivo chamado de math_test.go
para representar tais testes, e dizer aos futuros desenvolvedores (e também para o sistema do Go), que tal arquivo é usado para testar todas (ou algumas) funções de math.go
.
No meu caso, eu vou criar um novo arquivo chamado math_test.go
que vai existir na mesma pasta do arquivo math.go
:

Já dentro do arquivo math_test.go
, eu vou criar uma função chamada de TestDiscoverMedia
para representar um possível teste relacionado a função DiscoverMedia
que existe dentro do arquivo math.go
.
package math
import "testing"
const defaultError = "Valor Esperado %v, encontrado %v"
func TestDiscoverMedia(t *testing.T) {
expectedValue := 7.28
value := DiscoverMedia(7.2, 9.9, 6.1, 5.9)
if value != expectedValue {
t.Errorf(defaultError, expectedValue, value)
}
}
Observação: certifique-se de que a função DiscoverMedia
do arquivo math.go
comece com a letra maiúscula. Precisamos disso para que tal função possa ser acessada por outros módulos (arquivos).
Agora vamos ás explicações 😉
Primeiro de tudo, sempre importe o módulo "testing" antes de escrever suas funções, pois é com ele que o GoLang vai saber que o arquivo se trata de um módulo de testes:
import "testing"
Segundo, e não menos importante, certifique-se de que tanto seu módulo, quanto o módulo de testes fazem parte do mesmo pacote.
No meu caso, como estou usando o pacote math
, eu preciso que meu módulo original e de testes façam parte desse mesmo pacote:
package math
Por fim, eu criei uma lógica (função) para testar o DiscoverMedia
:
const defaultError = "Valor Esperado %v, encontrado %v"
func TestDiscoverMedia(t *testing.T) {
expectedValue := 7.28
value := DiscoverMedia(7.2, 9.9, 6.1, 5.9)
if value != expectedValue {
t.Errorf(defaultError, expectedValue, value)
}
}
No comando acima, nós estamos definindo uma mensagem de erro padrão (defaultError) com placeholders para o valor esperado e o valor obtido:
const defaultError = "Valor Esperado %v, encontrado %v"
Isso vai nos ajudar a "cuspir" uma mensagem intuitiva de volta pra gente, isto é, caso o teste retorne algum erro.
Em seguida, criamos uma função de teste chamada TestDiscoverMedia
:
func TestDiscoverMedia(t *testing.T) {
expectedValue := 7.28
value := DiscoverMedia(7.2, 9.9, 6.1, 5.9)
if value != expectedValue {
t.Errorf(defaultError, expectedValue, value)
}
}
Que recebe por parâmetro um atributo de testes, diretamente do módulo "testing", e:
- Atribui a
expectedValue
o valor que você espera queDiscoverMedia
retorne (7.28). - Chama
DiscoverMedia(7.2, 9.9, 6.1, 5.9)
e guarda o resultado emvalue
. - Compara
value
comexpectedValue
.
No final da função, se os dois valores não forem iguais, eu chamo o t.Errorf
de modo a passar o template de erro e os dois valores de volta, fazendo o teste falhar exibindo no terminal algo como:
Valor Esperado 7.28, encontrado X.YZ
Em resumo, o comando que acabamos de criar acima é um teste que garante que, para aqueles quatro parâmetros, a função DiscoverMedia
devolva exatamente 7.28
, e em caso de discrepância, reporta o erro formatado.
Para rodar esse código, certifique-se de que o seu terminal está apontado para dentro da pasta math
antes de executar o código abaixo:
go test math_test.go math.go
A ideia é usar o comando go test
mais o nome dos arquivos (módulo original + módulo de testes) para executar seus testes.
Se tudo der certo, você verá uma mensagem similar a esta no terminal:
ok command-line-arguments 0.132s
Caso contrário (experimente mudar o valor da variável expectedValue), você poderá se deparar com uma mensagem de FAIL
:
--- FAIL: TestDiscoverMedia (0.00s)
math_test.go:11: Valor Esperado 7.58, encontrado 7.28
FAIL
FAIL command-line-arguments 0.115s
FAIL
E pronto, você acabou de criar o seu primeiro teste unitário 🥳
Criando diversos cenários de testes (DataSets)
Quando estamos trabalhando com GoLang, é muito comum usarmos table-driven tests ("datasets" de teste) para organizarmos vários casos de testes de forma clara e concisa.
Tais datasets nos ajudam a criar um conjunto estruturado de casos, em que em vez de precisarmos escrever uma estrutura condicional (if...else) para cada cenário de testes, nós conseguimos agrupar todos os inputs, outpurs esperados e metadados numa slice
ou struct
.
Para exemplificar isso, dentro da pasta do nosso projeto, vamos criar uma nova pasta chamada de datasets, onde dentro dela, criaremos um novo arquivo chamado de strings_test.go
que representa o nosso primeiro dataset:

Segue a lógica do arquivo strings_test.go
:
package strings
import (
"testing"
"strings"
)
// msgIndex é o template de erro com placeholders:
// %s → texto completo
// %s → substring buscada
// %d → índice esperado
// %d → índice encontrado
const msgIndex = "%s (parte: %s) - índices: esperado (%d) <> encontrado (%d)."
func TestIndex(t *testing.T) {
// 1) Definição do “dataset”: slice de structs com três campos
tests := []struct {
text string // texto onde vamos buscar
part string // substring a buscar
expected int // índice esperado de retorno
}{
{"Micilini is Best!", "Micilini", 0}, // começa na posição 0
{"", "", 0}, // string vazia em string vazia → índice 0
{"Hey", "hey", -1}, // busca case‑sensitive: não encontra → -1
{"GoLang", "L", 2}, // “L” começa no índice 2 em “GoLang”
}
// 2) Loop que executa cada caso de teste
for _, test := range tests {
// Loga o caso atual (útil para debugging)
t.Logf("Result: %v", test)
// 3) Chama a função que está sob teste
current := strings.Index(test.text, test.part)
// 4) Compara resultado com o esperado
if current != test.expected {
// Emite um erro formatado se der mismatch
t.Errorf(
msgIndex,
test.text,
test.part,
test.expected,
current,
)
}
}
}
Agora vamos ás explicações 😊
- Importações: traz o pacote testing (infra de testes do Go) e
strings
(que contém Index). - Constante de mensagem: a variável
msgIndex
faz a formatação do erro. - Dataset:
tests := []struct{…}{…}
é usado para criar uma lista de cenários. - Execução genérica: já o comando
for
roda cada caso, chamastrings.Index
e guarda emcurrent
. - Verificação: por fim, realizamos uma verificação simples onde o
current != expected, t.Errorf
quebra o teste mostrando exatamente qual caso e qual índice deu errado.
Observação: para rodar o teste acima, certifique-se de que o seu terminal está dentro da pasta datasets, e não esqueça de rodar o comando go test strings_test.go
.
Se você percebeu, no código acima nós criamos diversos cenários diferentes dentro de uma única struct
, o que nos ajuda a executar uma grande gama de testes de uma única vez 😀
Testando os tipos de arquitetura
Como vimos em uma das primeiras lições dessa jornada, existem certos códigos que funcionam apenas no Windows, outros em Linux e alguns no Mac.
E sim, se você estiver no Windows por exemplo, e executar um go build, a aplicação final será um executável .exe. Já no Linux e Mac eles possuem extensões diferentes.
Pensando nisso, que tal desenvolvermos um módulo de testes que verifica se a arquitetura e o sistema operacional são compatíveis com a aplicação que criamos?
Para isso, dentro da pasta do nosso projeto, criaremos mais uma pasta chamada de arch, onde dentro dela vamos criar um novo arquivo chamado de arch_test.go
:

A ideia deste módulo de testes é:
- Detectar se o sistema operacional em tempo de execução é Windows, Linux ou macOS.
- Verificar se uma pasta "típica" daquele SO existe, como por exemplo, C:\Windows no Windows, /usr no Linux e /Applications no macOS.
- Por fim, emitiremos um
Skip
se for um SO não suportado, ou um erro se a pasta esperada não for encontrada.
Sendo assim, vamos usar a seguinte lógica dentro do arquivo arch_test.go
:
package arch
import (
"os"
"path/filepath"
"runtime"
"testing"
)
// mapeia cada SO ao path que deve existir
var requiredPaths = map[string]string{
"windows": `C:\Windows`,
"linux": "/usr",
"darwin": "/Applications",
}
func TestArchSupport(t *testing.T) {
osName := runtime.GOOS // "windows", "linux" ou "darwin"
arch := runtime.GOARCH // "amd64", "386", "arm64", etc.
t.Logf("SO detectado: %s | ARCH detectada: %s", osName, arch)
// 1) Verifica se suportamos esse SO
basePath, supported := requiredPaths[osName]
if !supported {
t.Skipf("SO não suportado: %s — este programa não roda aqui", osName)
}
// 2) Monta um exemplo de subpasta dentro do path base
// (por exemplo, "%SystemRoot%\System32" no Windows ou "/usr/local/bin" no Unix)
var testPath string
switch osName {
case "windows":
testPath = filepath.Join(basePath, "System32")
case "linux", "darwin":
testPath = filepath.Join(basePath, "local", "bin")
}
// 3) Verifica se a pasta existe
info, err := os.Stat(testPath)
if err != nil {
if os.IsNotExist(err) {
t.Errorf("Pasta esperada não encontrada em %s para %s/%s", testPath, osName, arch)
} else {
t.Errorf("Erro checando %s: %v", testPath, err)
}
return
}
if !info.IsDir() {
t.Errorf("Caminho existe mas não é pasta: %s", testPath)
return
}
// 4) Se chegou até aqui, está tudo OK
t.Logf("✔ Pasta %s encontrada. Arquitetura %s/%s suportada.", testPath, osName, arch)
}
No código acima, nós usamos os comandos runtime.GOOS
e runtime.GOARCH
para saber onde o binário está rodando.
Em seguida, definimos um mapeamento de paths, onde o requiredPaths
armazena o path "base" que deve existir em cada SO.
Se o runtime.GOOS
não estiver em requiredPaths
, invocamos t.Skipf
, registrando que o teste não se aplica a essa plataforma.
Por fim, usamos o comando os.Stat
para verificar se o caminho existe e é de fato uma pasta. Se não, o teste falha com t.Errorf
.
- t.Logf mostra informações de diagnóstico (SO, ARCH, paths).
- t.Skipf deixa claro que aquele SO não é suportado.
- t.Errorf reporta erro de forma clara, apontando qual pasta não foi encontrada.
Lembre-se que para rodar este código, seu terminal precisa estar aberto dentro da pasta arch, e em seguida você precisa executar o comando:
go test arch_test.go
Executando testes de forma paralela
Durante seus testes, você pode fazer o uso do comando t.Parallel()
, que serve para dizer ao framework de testes do Go que aquele caso de teste pode (e deve, se possível) ser executado concorrentemente com outros testes que também chamem t.Parallel()
.
O uso deste comando ajuda a reduzir o tempo total da suíte de testes, aproveitando múltiplos núcleos da sua máquina.
Vejamos um exemplo simples:
func TestA(t *testing.T) {
t.Parallel() // marca todo TestA para rodar em paralelo
// ... código de teste A ...
}
func TestB(t *testing.T) {
// setup sequencial aqui (se precisar)
t.Parallel() // a partir daqui, TestB roda em paralelo com TestA
// ... código de teste B ...
}
Caso você queira fazer isso com subtests basta usar uma lógica similar a esta que está abaixo:
func TestIndexVariants(t *testing.T) {
cases := []struct{name, text, part string; want int}{ /*…*/ }
for _, tc := range cases {
tc := tc // capturar por valor
t.Run(tc.name, func(t *testing.T) {
t.Parallel() // cada subteste roda concorrentemente
got := strings.Index(tc.text, tc.part)
if got != tc.want {
t.Errorf("…")
}
})
}
}
Para rodar múltiplos arquivos de testes em GoLang basta executar o código na raiz do seu projeto, que de forma automática, ele irá buscar e testar todos os arquivos de testes existentes em pastas e subpastas:
go test ./...
Se você quiser ver um output detalhado de cada teste, é só acrescentar a flag -v
da seguinte forma:
go test -v ./...
Gerando um Relatório de Cobertura de Testes em Go
O termo cobertura de testes (ou coverage em ingles) indica o quanto o seu código está sendo efetivamente exercitado pelos testes automatizados.
Um relatório como esses nos ajuda a:
- Identificar trechos não testados,
- Garantir qualidade e confiabilidade do nosso sistema,
- E facilitar auditorias e revisão de código.
Para executar um teste de cobertura é bem simples, basta seguir o passo a passo abaixo 🙃
1) Assegure-se de ter testes no seu projeto (arquivos com sufixo _test.go) cobrindo as funções e métodos principais.
2) Execute um teste de cobertura em todos os arquivos (certifique-se de que seu terminal está apontado para a pasta raiz do seu projeto):
go test -cover ./...
O comando acima retornará algo como:
ok github.com/seuuser/seuprojeto 0.123s coverage: 72.3% of statements
3) Gere um arquivo de perfil de cobertura:
go test -coverprofile=coverage.out ./...
-coverprofile=coverage.out
: salva os dados emcoverage.out
../...
: percorre todos os pacotes do projeto.
4) Visualize o relatório em HTML: Com o coverage.out pronto, basta gerar uma página interativa em formato HTML.
go tool cover -html=coverage.out -o coverage.html
Depois, abra coverage.html
no seu navegador para ver, linha a linha, onde faltam testes a serem feitos.
Se você deseja um relatório de cobertura por função, você pode usar a flag -func
da seguinte forma:
go tool cover -func=coverage.out
E pronto, seguindo esse passo a passo, você tem um relatório de cobertura de testes integrado ao seu sistema atual 🤩
Repositório da lição
Todos os arquivos relacionados com esta lição, podem ser encontrados nos seguintes repositórios abaixo:
Conclusão
Nesta lição, você aprendeu um pouco mais sobre testes automatizados em GoLang e como criá-los dentro da sua aplicação, tornando-a mais robusta e confiável 😉
Até a próxima lição!