Testes Automatizados em Ruby: Minitest vs RSpec

Testes automatizados são uma parte fundamental do desenvolvimento Ruby moderno. Eles não apenas garantem que nosso código funcione conforme esperado, mas também nos ajudam a escrever código mais limpo e bem estruturado. Neste tutorial, vamos explorar os dois principais frameworks de teste em Ruby: minitest e RSpec.

Por Que Testes Unitários São Essenciais

Testes unitários focam em pequenos pedaços de código, geralmente métodos individuais ou branches dentro de métodos. Eles oferecem várias vantagens importantes:

Detecção Precoce de Bugs: Quando você testa o código enquanto o escreve, é mais provável encontrar bugs enquanto ainda estão frescos na sua mente. Além disso, quando um bug aparece, você só precisa procurar em algumas linhas de código.

Design Melhor: Pensar sobre testes naturalmente leva você a criar designs melhores e mais desacoplados. O processo de escrever testes expõe fraquezas na estrutura do código.

Testando com Minitest: O Framework Padrão

Minitest é o framework de testes que vem com a biblioteca padrão do Ruby. Vamos começar com um exemplo prático criando uma classe para números romanos:

# Classe para converter números para algarismos romanos
class NumeroRomano
  MAX_ROMANO = 4999
  
  def initialize(valor)
    if valor <= 0 || valor > MAX_ROMANO
      fail "Valores romanos devem ser > 0 e <= #{MAX_ROMANO}"
    end
    @valor = valor
  end
  
  FATORES = [
    ["m", 1000], ["cm", 900], ["d", 500],
    ["c", 100], ["x", 10], ["i", 1]
  ]
  
  def to_s
    valor = @valor
    romano = ""
    FATORES.each do |codigo, fator|
      contagem, valor = valor.divmod(fator)
      romano << (codigo * contagem)
    end
    romano
  end
end

Criando Testes com Minitest

Agora vamos criar testes para nossa classe. Minitest usa assertions (afirmações) para verificar se o código funciona conforme esperado:

require_relative "numero_romano"
require "minitest/autorun"

class TestNumeroRomano < Minitest::Test
  def assert_valor_romano(numeral_romano, numero_arabico)
    assert_equal(numeral_romano, NumeroRomano.new(numero_arabico).to_s)
  end

  def test_conversoes_simples
    assert_valor_romano("i", 1)
    assert_valor_romano("ii", 2)
    assert_valor_romano("iv", 4)
    assert_valor_romano("ix", 9)
  end

  def test_validacao_intervalo
    # Sem exceção para valores válidos
    NumeroRomano.new(1)
    NumeroRomano.new(4999)
    
    # Mas exceção para valores inválidos
    assert_raises(RuntimeError) { NumeroRomano.new(0) }
    assert_raises(RuntimeError) { NumeroRomano.new(5000) }
  end
end

Setup e Teardown em Minitest

Para código de configuração comum, minitest oferece métodos setup e teardown que são executados antes e depois de cada teste:

class TestPlaylistBuilder < Minitest::Test
  def setup
    @banco_dados = DBI.new("DBI:mysql:playlists")
    @construtor_playlist = ConstrutorPlaylist.new(@banco_dados)
  end

  def teardown
    @construtor_playlist.close
  end

  def test_playlist_vazia
    assert_empty(@construtor_playlist.playlist)
  end
end

RSpec: Framework Orientado ao Comportamento

RSpec oferece uma abordagem diferente com foco no comportamento esperado do código. Vamos ver um exemplo prático criando um sistema de pontuação de tênis:

require_relative "pontuador_tenis"

RSpec.describe PontuadorTenis do
  describe "pontuação básica" do
    let(:pontuador) { PontuadorTenis.new }

    it "começa com pontuação 0-0" do
      expect(pontuador.pontuacao).to eq("0-0")
    end

    it "faz a pontuação 15-0 se o servidor ganha um ponto" do
      pontuador.dar_ponto_para(:servidor)
      expect(pontuador.pontuacao).to eq("15-0")
    end

    it "levanta erro se não conhece o jogador" do
      expect { pontuador.dar_ponto_para(:arbitro) }.to raise_error(RuntimeError)
    end
  end
end

Implementação da Classe PontuadorTenis

class PontuadorTenis
  JOGADORES = %i[servidor receptor]

  def initialize
    @pontuacao = {servidor: 0, receptor: 0}
  end

  def pontuacao
    "#{@pontuacao[:servidor] * 15}-#{@pontuacao[:receptor] * 15}"
  end

  def dar_ponto_para(jogador)
    raise "Jogador desconhecido #{jogador}" unless JOGADORES.include?(jogador)
    @pontuacao[jogador] += 1
  end
end

Mock Objects: Simulando Dependências

Mock objects são objetos que simulam a API de objetos existentes no sistema. Eles são especialmente úteis para testar comportamento sem depender de recursos externos caros ou frágeis.

Mocks em Minitest

def setup
  @banco_dados = Minitest::Mock.new
  @banco_dados.expect(:conectar, true)
  @banco_dados.expect(:desconectar, false)
  @construtor_playlist = ConstrutorPlaylist.new(@banco_dados)
end

def teardown
  @banco_dados.desconectar
  @banco_dados.verify  # Verifica se todas as expectativas foram atendidas
end

Mocks em RSpec

# Criando um double simples
objeto = double(custo: "barato", nome: "banana")

# Verificando se métodos foram chamados
expect(objeto).to receive(:custo).and_return("barato")
objeto.custo

# Stubbing métodos em objetos existentes
kermit = Muppet.new
allow(kermit).to receive(:saudacao).and_return("Oi pessoal")

Matchers do RSpec: Expressividade Natural

RSpec oferece uma sintaxe rica de matchers que torna os testes mais legíveis e próximos da linguagem natural:

# Matchers básicos de lógica
expect(valor).to be_truthy
expect(valor).to be_an_instance_of(Produto)
expect(valor.preco).to be > 10

# Matchers para dados estruturados
expect(array).to contain_exactly(:a, :b, :c)
expect(hash).to include(chave: valor)
expect(string).to start_with("abc")

# Matchers dinâmicos
expect(livro).to be_uma_brochura  # Procura por livro.brochura?
expect(livro).to have_autor("Dave")     # Procura por livro.has_autor?("Dave")

Organizando e Executando Testes

Uma boa organização de testes é fundamental para projetos de grande escala. Aqui estão algumas práticas recomendadas:

Estrutura de Diretórios

projeto/
  lib/
    numero_romano.rb
    pontuador_tenis.rb
  test/
    test_numero_romano.rb
    test_pontuador_tenis.rb
  spec/
    numero_romano_spec.rb
    pontuador_tenis_spec.rb

Executando Testes

# Executando testes minitest
ruby -I lib test/test_numero_romano.rb

# Executando testes específicos
ruby test/test_numero_romano.rb -n test_conversoes_simples

# Executando testes RSpec
rspec spec/numero_romano_spec.rb

Minitest vs RSpec: Quando Usar Cada Um

Use Minitest quando:

  • Preferir simplicidade e compatibilidade com outros frameworks de teste
  • Quiser usar apenas a biblioteca padrão do Ruby
  • Estiver trabalhando em projetos menores ou com equipes iniciantes

Use RSpec quando:

  • Quiser uma sintaxe mais expressiva e próxima da linguagem natural
  • Precisar de recursos avançados de mocking e stubbing
  • Estiver focado em desenvolvimento orientado ao comportamento (BDD)

Conclusão

Testes automatizados são uma ferramenta essencial para qualquer desenvolvedor Ruby sério. Tanto minitest quanto RSpec oferecem excelentes capacidades, cada um com suas próprias vantagens. Minitest oferece simplicidade e está incluído na biblioteca padrão, enquanto RSpec fornece expressividade e recursos avançados.

O mais importante é começar a escrever testes, independentemente do framework escolhido. À medida que você se familiariza com os testes, descobrirá que eles não apenas melhoram a qualidade do seu código, mas também tornam o desenvolvimento mais rápido e confiável a longo prazo.

Lembre-se: testes não são apenas sobre encontrar bugs - eles são sobre escrever código melhor, mais limpo e mais maintível. Eles são um investimento no futuro do seu projeto.

Comentários (0)

Nenhum comentário:

Postar um comentário