Imagine começar a construir uma casa sem um projeto. Estranho, né? Agora, pense em escrever código sem antes definir como ele deve se comportar. O Test-Driven Development (TDD) é como ter um projeto para sua casa: você escreve testes antes do código funcional. O ciclo de TDD é composto por três etapas:
- 🚫 Red: Escreva um teste que falha porque a funcionalidade ainda não foi implementada.
- ✅ Green: Implemente o código necessário para fazer o teste passar.
- 🔄 Refactor: Melhore a estrutura do código mantendo todos os testes passando.
Benefícios do TDD
Confiança no Código: Você sabe que seu código funciona porque ele passou por testes rigorosos. Em um cenário do mundo real, isso significa que quando você faz uma alteração no código, seus testes garantem que não quebrou nada que já estava funcionando. Isso é especialmente importante em aplicações críticas, como sistemas financeiros ou de saúde, onde uma falha pode ter consequências sérias.
Design de Qualidade: TDD força você a pensar sobre o design e a interface da sua aplicação. No mundo real, isso resulta em código mais modular e mais fácil de manter. Por exemplo, se você está desenvolvendo um serviço de e-commerce, a prática de TDD ajuda a garantir que cada componente, como o carrinho de compras, o sistema de pagamento e o gerenciamento de estoque, seja bem definido e independente.
Feedback Rápido: Descobre falhas rapidamente, evitando surpresas desagradáveis mais tarde. Em um projeto real, isso significa que você pode corrigir bugs antes que eles se tornem grandes problemas. Imagine que você está desenvolvendo uma aplicação de streaming de vídeo (como a PlutoTV, onde você trabalha). Com TDD, você pode identificar e corrigir problemas de buffering ou de qualidade de vídeo de maneira eficiente, antes que impactem os usuários finais.
Redução de Debugging: Menos tempo gasto em debugging e mais tempo em desenvolvimento de novas funcionalidades. No mundo real, isso se traduz em maior produtividade e prazos de entrega mais curtos. Imagine que você está adicionando uma nova funcionalidade de recomendação de vídeos baseada em IA. Com TDD, você pode focar em desenvolver e treinar o modelo de IA, sabendo que a integração com o sistema existente está bem testada.
Documentação Viva: E por ultimo, mas talvez o mais importante... Os testes atuam como uma documentação viva do comportamento esperado do sistema. Isso é extremamente útil quando novos desenvolvedores entram na equipe ou quando você retorna a um projeto após algum tempo. No mundo real, se um novo desenvolvedor se juntar à sua equipe de backend, eles podem entender rapidamente como as APIs de recomendação de vídeo funcionam apenas lendo os testes.
Mão na massa
Chega de falar e vamos colocar a mão no código. Primeiro vamos começar pelo básico, uma função que calcula o "fatorial" de um número. O fatorial de um número é uma operação matemática que multiplica o número por todos os inteiros positivos menores que ele e para representarmos o fatorial usamos o exclamação "!".
1 - Escrever o teste que queremos para calcular o fatorial, nessa primeira etapa ele falhará pois ainda não temos a função Factorial
:
package main
import (
"testing"
)
func TestFactorial(t *testing.T) {
result := Factorial(5)
expected := 120
if result != expected {
t.Errorf("Expected: %d, Actual: %d", expected, result)
}
}
Se rodarmos o teste, ele retornará o seguinte resultado de erro:
./tdd_test.go:8:12: undefined: Factorial
Atingimos o 🚫 Red.
2 - Agora vamos implementar nossa função que esta faltando e que calcula o fatorial:
package tdd
func Factorial(n int) int {
if n == 0 {
return 1
}
result := 1
for i := 1; i <= n; i++ {
result *= i
}
return result
}
Após executar o teste, ele passará. Atingimos o ✅ Green.
3 - Agora vamos fazer o Refactor de nosso código. Que tal implementarmos o factorial de uma forma recursiva e deixar nosso código mais simples? Podemos utilizar uma recursão para deixar o código mais simples.
package tdd
func Factorial(n int) int {
if n == 0 {
return 1
}
return n * Factorial(n-1)
}
Aproveitando o Refactor, podemos também reescrever nosso teste usando o Table Driven e também adicionar mais testes e cobrir um número maior de casos.
package tdd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestFactorial(t *testing.T) {
tests := []struct {
name string
input int
expected int
}{
{
name: "should calculate factorial for 5",
input: 5,
expected: 120,
},
{
name: "should calculate factorial for 0",
input: 0,
expected: 1,
},
{
name: "should calculate factorial for 3",
input: 3,
expected: 6,
},
}
for _, tt := range tests {
result := Factorial(tt.input)
assert.Equal(t, tt.expected, result)
}
}
E agora sim, nosso 🔄 Refactor está feito...
Caramba Rafa, mas o que é esse tal de Table Driven? Bora nos aprofundar um pouco sobre uma das técnicas mais usadas nos testes em Go.
Table-Driven Tests
Table-Driven Tests são uma maneira de tornar seus testes mais organizados e reutilizáveis. Você define uma tabela com entradas e saídas esperadas, e o mesmo teste é executado para cada par de entrada/saída.
Uma coisa que eu amo fazer, é brincar com exemplos do mundo real para que todos consigam entender do que estamos falando, então imagine que você é um chef de cozinha e precisa testar várias receitas de um novo prato. Em vez de fazer um teste separado para cada ingrediente e combinação, você faz uma tabela onde lista todas as combinações possíveis de ingredientes e suas respectivas expectativas de sabor. Agora, ao invés de experimentar uma por uma, você segue essa tabela, economizando tempo e garantindo que todas as combinações sejam testadas.
É exatamente isso que fazemos com table-driven tests no mundo da programação!
Estrutura Básica
Geralmente partimos da seguinte base:
- Descrição/Nome (Opcional): Explicação breve do caso de teste.
- Entradas: Valores que serão passados para a função ou método.
- Saída Esperada: Resultado que esperamos obter com as entradas fornecidas.
package main
import (
"testing"
)
// Struct que define um caso de teste
type testCase struct {
name string
input int
expected int
}
Bora ver um exemplo prático de como podemos aplicar, vamos considerar uma função mais complexa que calcula o preço final de um produto após aplicar desconto e imposto. Vamos seguir com o TDD.
1 - Criamos um struct testCase para armazenar os dados de cada teste, incluindo um nome para nosso teste, entradas (basePrice
, discount
, taxRate
) e a saída esperada (expected
).
package main
type priceTestCase struct {
name string
basePrice float64
discount float64
taxRate float64
expected float64
}
2 - Criamos a função de teste TestCalculatePrice
e definimos uma slice de priceTestCase
chamada tests
, onde cada elemento representa um caso de teste específico.
package main
type priceTestCase struct {
name string
basePrice float64
discount float64
taxRate float64
expected float64
}
func TestCalculatePrice(t *testing.T) {
tests := []priceTestCase{
{
"should calculate final price without discount and tax",
100.0,
0.0,
0.0,
100.0,
},
{
"should calculate final price with 10% off and without tax",
100.0,
10.0,
0.0,
90.0,
},
{
"should calculate final price without discount and 10% tax",
100.0,
0.0,
10.0,
110.0,
},
{
"should calculate final price with 10% off and 10% tax",
100.0,
10.0,
10.0,
99.0,
},
{
"should calculate final price with discount and tax applied",
200.0,
15.0,
8.0,
183.6,
},
}
}
3 - Criamos um loop for
para iterar sobre cada caso de teste na tabela. Dentro do loop, usamos t.Run
para criar subtestes, o que facilita a identificação de qual caso falhou. E por fim fazemos o assert para descobrir se nosso tt.expected
é igual ao resultado que a função CalculatePrice
retornou.
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
type priceTestCase struct {
name string
basePrice float64
discount float64
taxRate float64
expected float64
}
func TestCalculatePrice(t *testing.T) {
tests := []priceTestCase{
{
"should calculate final price without discount and tax",
100.0,
0.0,
0.0,
100.0,
},
{
"should calculate final price with 10% off and without tax",
100.0,
10.0,
0.0,
90.0,
},
{
"should calculate final price without discount and 10% tax",
100.0,
0.0,
10.0,
110.0,
},
{
"should calculate final price with 10% off and 10% tax",
100.0,
10.0,
10.0,
99.0,
},
{
"should calculate final price with discount and tax applied",
200.0,
15.0,
8.0,
183.6,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := CalculatePrice(tt.basePrice, tt.discount, tt.taxRate)
assert.Equal(t, tt.expected, result)
})
}
}
4 - Por fim, implementamos a função CalculatePrice
, que deve receber como parâmetros 3 valores float: basePrice
, discount
, taxRate
.
package main
func CalculatePrice(basePrice float64, discount float64, taxRate float64) float64 {
discountedPrice := basePrice - (basePrice * discount / 100)
return discountedPrice + (discountedPrice * taxRate / 100)
}
E agora quando rodarmos os testes esse será nosso output:
--- PASS: TestCalculatePrice (0.00s)
--- PASS: TestCalculatePrice/should_calculate_final_price_without_discount_and_tax (0.00s)
--- PASS: TestCalculatePrice/should_calculate_final_price_with_10%_off_and_without_tax (0.00s)
--- PASS: TestCalculatePrice/should_calculate_final_price_without_discount_and_10%_tax (0.00s)
--- PASS: TestCalculatePrice/should_calculate_final_price_with_10%_off_and_10%_tax (0.00s)
--- PASS: TestCalculatePrice/should_calculate_final_price_with_discount_and_tax_applied (0.00s)
PASS
Process finished with the exit code 0
Conclusão
Essa abordagem permite a definição clara de diversos cenários de teste, reduzindo a repetição de código e facilitando a manutenção e escalabilidade dos testes.
- Facilidade de Adição: Para adicionar um novo caso de teste, basta adicionar um novo elemento à slice tests.
- Leitura Clara: A estrutura clara dos casos de teste facilita a compreensão de quais cenários estão sendo testados.
- Manutenção Simples: Modificar os casos de teste ou as expectativas é simples e direto.
Espero que vocês gostem e caso queiram mais exemplos de teste, deixem seus comentários :)
Top comments (0)