DEV Community

Abraão Carvalho
Abraão Carvalho

Posted on

Boas e Más práticas na escrita de testes usando RSpec no Ruby on Rails

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
aristotelesbr profile image
Aristóteles Coutinho

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 o context ao invés do describe? Melhor, daria pra usar ambos? Muito bom, parabéns pelo post.

Collapse
 
abraaocrvlh42 profile image
Abraão Carvalho • Edited

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.