Como dito anterioremente, a escrita de testes é uma prática fundamental no desenvolvimento de software, pois garante a qualidade e a estabilidade do código ao longo do tempo. No contexto do Ruby on Rails, uma estrutura popular para desenvolver aplicações web, o RSpec é uma biblioteca de testes muito utilizada. No entanto, é importante conhecer as boas práticas para escrever testes eficazes e evitar más práticas que podem comprometer a qualidade do código e a manutenibilidade do sistema.
Boas Práticas
Escrever testes claros e legíveis: Os testes devem ser compreensíveis para qualquer desenvolvedor que os leia. Use nomes descritivos para os testes e métodos auxiliares, e mantenha a lógica dos testes simples e direta.
Exemplo:
describe UserController do
describe "#create" do
it "creates a new user" do
# Test implementation
end
end
end
Testar comportamentos, não implementações
Concentre-se nos comportamentos esperados do código, não em como esses comportamentos são implementados. Isso torna os testes menos frágeis e mais fáceis de manter à medida que o código evolui.
Vamos considerar um exemplo de como testar o comportamento de um método calculate_total de uma classe Order. Suponha que este método seja responsável por calcular o total de uma ordem com base nos itens presentes nela. Em vez de testar detalhes específicos da implementação do cálculo, podemos nos concentrar no comportamento esperado do método, como garantir que o total seja calculado corretamente com diferentes itens e quantidades.
# order.rb
class Order
attr_reader :items
def initialize
@items = []
end
def add_item(item, quantity)
@items << { item: item, quantity: quantity }
end
def calculate_total
total = 0
@items.each do |item|
total += item[:item].price * item[:quantity]
end
total
end
end
Aqui está um exemplo de teste que se concentra no comportamento esperado do método calculate_total em vez de detalhes da implementação:
# order_spec.rb
require 'order'
RSpec.describe Order do
describe '#calculate_total' do
it 'calcula corretamente o total com um único item' do
order = Order.new
item = double('item', price: 10)
order.add_item(item, 2)
expect(order.calculate_total).to eq(20)
end
it 'calcula corretamente o total com vários itens' do
order = Order.new
item1 = double('item1', price: 10)
item2 = double('item2', price: 5)
order.add_item(item1, 2)
order.add_item(item2, 3)
expect(order.calculate_total).to eq(35)
end
end
end
Estamos testando o comportamento do método calculate_total ao adicionar diferentes itens à ordem e verificar se o total é calculado corretamente. Não estamos testando os detalhes da implementação do cálculo, como os cálculos específicos dentro do método. Estamos usando objetos simulados (doubles) para representar os itens, o que nos permite isolar o teste do comportamento do método em relação aos detalhes da implementação dos itens.
Usar contextos para organizar os testes
Divida os testes em contextos significativos que representem diferentes situações ou estados do sistema. Isso ajuda a manter os testes organizados e facilita a identificação de falhas.
Vamos considerar o exemplo utilizado acima para testar uma classe Order com métodos para adicionar e remover itens, e um método para calcular o total da ordem. Vamos organizar os testes em contextos diferentes para representar diferentes situações de uma ordem.
Exemplo:
# order.rb
class Order
attr_reader :items
def initialize
@items = []
end
def add_item(item, quantity)
@items << { item: item, quantity: quantity }
end
def remove_item(item)
@items.delete(item)
end
def calculate_total
total = 0
@items.each do |item|
total += item[:item].price * item[:quantity]
end
total
end
end
Aqui está um exemplo de como podemos usar contextos para organizar os testes:
# order_spec.rb
require 'order'
RSpec.describe Order do
describe 'com ordem vazia' do
let(:order) { Order.new }
it 'tem um total de 0' do
expect(order.calculate_total).to eq(0)
end
it 'não permite remover itens' do
item = { item: 'produto', quantity: 2 }
order.add_item(item, 2)
order.remove_item(item)
expect(order.items).to eq([item])
end
end
describe 'com ordem contendo itens' do
let(:order) { Order.new }
let(:item1) { { item: 'produto1', quantity: 2 } }
let(:item2) { { item: 'produto2', quantity: 3 } }
before do
order.add_item(item1, 2)
order.add_item(item2, 3)
end
it 'calcula o total corretamente' do
expect(order.calculate_total).to eq(2 * item1[:item].price + 3 * item2[:item].price)
end
it 'permite remover itens' do
order.remove_item(item1)
expect(order.items).to eq([item2])
end
end
end
Usamos dois contextos diferentes um para uma ordem vazia e outro para uma ordem contendo itens. Cada contexto usa um bloco describe separado para agrupar os testes relacionados àquele contexto específico. Usamos let para definir variáveis de instância que são compartilhadas entre os testes dentro do mesmo contexto. Cada teste dentro de um contexto testa um aspecto específico do comportamento da ordem, facilitando a compreensão dos requisitos e comportamentos do sistema em diferentes situações.
Ao usar contextos para organizar os testes, tornamos os testes mais claros, concisos e fáceis de manter. Isso também ajuda a identificar rapidamente onde estão os problemas quando os testes falham, facilitando a resolução de problemas.
Manter os testes independentes e isolados
Cada teste deve ser independente dos outros e não deve depender do estado compartilhado entre os testes. Isso garante que os testes possam ser executados em qualquer ordem e em qualquer ambiente.
Para manter os testes independentes e isolados, é importante garantir que cada teste não dependa do estado criado por outros testes e que eles possam ser executados em qualquer ordem. Vamos considerar um exemplo de uma classe Calculator com um método add que soma dois números. Queremos garantir que os testes para este método sejam independentes e isolados.
# calculator.rb
class Calculator
def add(a, b)
a + b
end
end
Aqui está um exemplo de teste que demonstra a manutenção da independência e isolamento dos testes:
# calculator_spec.rb
require 'calculator'
RSpec.describe Calculator do
describe '#add' do
it 'soma dois números corretamente' do
calculator = Calculator.new
result = calculator.add(2, 3)
expect(result).to eq(5)
end
end
describe '#add' do
it 'soma corretamente quando um número é zero' do
calculator = Calculator.new
result = calculator.add(5, 0)
expect(result).to eq(5)
end
end
end
Cada teste está contido em um bloco describe separado. Mesmo que ambos os testes estejam testando o mesmo método add, eles são completamente independentes um do outro. Cada teste cria uma nova instância do Calculator, garantindo que eles não compartilhem estado entre si.
Não há dependência entre os resultados dos testes; um teste não depende do resultado de outro teste para passar.
Ao manter os testes independentes e isolados, garantimos que cada teste possa ser executado de forma independente e em qualquer ordem, o que facilita a identificação e resolução de problemas quando os testes falham. Isso também torna os testes mais robustos e menos propensos a quebrar com mudanças na implementação ou em outros testes.
Más Práticas
Testes frágeis e quebradiços
Evite testes que dependam de detalhes de implementação interna, como valores específicos de variáveis ou ordem de execução. Esses testes podem quebrar facilmente com pequenas alterações no código.
RSpec.describe UserController do
it 'deve criar um usuário com o nome fornecido' do
post :create, params: { user: { name: 'John' } }
expect(User.last.name).to eq('John')
end
end
Neste exemplo, o teste está dependendo diretamente do último usuário criado no banco de dados para garantir que o usuário foi criado corretamente. Isso torna o teste frágil, pois pode falhar facilmente se houver outros testes que criam usuários ou se a ordem de execução dos testes mudar.
Testes lentos e pesados
Testes que envolvem operações lentas, como chamadas de rede ou acesso a bancos de dados, podem tornar o processo de teste lento e tedioso. Procure maneiras de isolar essas operações lentas ou substituí-las por simuladores mais rápidos em seus testes.
RSpec.describe UserController do
it 'deve enviar um email de boas-vindas ao criar um usuário' do
allow(UserMailer).to receive(:welcome_email).and_return(double(deliver_now: true))
post :create, params: { user: { name: 'John', email: 'john@example.com' } }
expect(UserMailer).to have_received(:welcome_email).with(User.last)
end
end
Neste exemplo, o teste está verificando se um e-mail de boas-vindas é enviado ao criar um usuário. No entanto, ele está realmente disparando a lógica de envio de e-mail, o que pode tornar o teste lento e dependente da conexão com o servidor de e-mail.
Testes duplicados e redundantes
Evite duplicação de código nos testes. Se várias partes do código exigirem testes semelhantes, considere criar métodos auxiliares ou fábricas para reutilizar o código de teste.
RSpec.describe UserController do
it 'deve criar um usuário com o nome fornecido' do
post :create, params: { user: { name: 'John' } }
expect(User.last.name).to eq('John')
end
it 'deve criar um usuário com o email fornecido' do
post :create, params: { user: { email: 'john@example.com' } }
expect(User.last.email).to eq('john@example.com')
end
end
Neste exemplo, estamos repetindo a lógica de criação de usuário em múltiplos testes. Isso não apenas torna os testes mais verbosos, mas também os torna mais propensos a quebrar se a implementação da criação de usuário mudar.
Conclusão
Escrever testes eficazes é crucial para garantir a qualidade e a estabilidade do código em uma aplicação Ruby on Rails. Seguir boas práticas, como escrever testes claros e legíveis, manter a independência entre os testes e testar comportamentos em vez de implementações, ajuda a criar testes robustos e fáceis de manter. Evitar más práticas, como testes frágeis e lentos, é igualmente importante para garantir a eficácia dos testes ao longo do tempo. Ao aplicar essas práticas ao usar o RSpec em Ruby on Rails, os desenvolvedores podem melhorar a qualidade do código e facilitar a manutenção do sistema.
Top comments (2)
Você mencionou o uso de contexto para organizar os testes. Se não me engano o RSpec tem um método chamado
context
. Daria pra usar ocontext
ao invés dodescribe
? Melhor, daria pra usar ambos? Muito bom, parabéns pelo post.Sim, você está absolutamente correto ! No RSpec, você pode usar tanto describe quanto context para organizar seus testes. Ambos servem essencialmente para o mesmo propósito: agrupar testes relacionados em uma estrutura hierárquica e criar contextos semânticos para os testes.
A principal diferença entre describe e context é semântica. describe é geralmente usado para descrever a funcionalidade de uma classe ou método, enquanto context é usado para descrever diferentes contextos ou situações em que essa funcionalidade pode ser testada.
PS: Fico lisonjeado com elogio, muito obrigado.