Ruby e RSpec: melhorando a legibilidade de seus testes

Publicado por Luiz Cezer Marrone Filho no dia dev

Legibilidade dos specs

Durante o desenvolvimento de software é importante que a equipe se mantenha focada nas entregas, escalabilidade, performance, usabilidade e também na legibilidade do código que está sendo escrito.

Código bem escrito e testado implica em facilidade de manutenção, código menos engessado e uma maior flexibilidade para futuras mudanças. Porém, de nada adianta um código bem escrito e testado se a codificação dos testes é ruim. A qualidade aplicada para escrita de código de produção também deve se estender à qualidade da codificação dos testes.

Fazendo o uso correto das estruturas do RSpec e com o padrão Four Phase Test é possível melhorar a legibilidade dos testes.

O que é Four Phase Test ?

Four Phase Test é um padrão de codificação de testes que pode ser aplicado em qualquer linguagem e qualquer ferramenta de testes. O conceito do padrão está em dividir o teste em 4 fases, sendo elas:

Setup

Nesta etapa são carregados todos os elementos que formam as pré-condições do teste, como por exemplo, carregamento de algum objeto, arquivos, mocks ou dependências gerais para o teste.

Exercise

Nesta etapa ocorre a execução do código que será testado.

Verify

Nesta etapa é feita a verificação do que se deseja testar.

Teardown

Por fim, tudo que foi carregado na fase de Setup pode ser descarregado, resetando o teste.

De forma geral, um teste que aplica todas essas etapas fica dessa forma:

1
2
3
4
5
6
7
8
9
test do
  setup

  exercise

  verify

  teardown
end

Como aplicar o padrão Four Phase Test ?

Tomando como base o exemplo abaixo, o código do teste, embora pequeno, não tem separação de linhas identificando o que cada parte representa. Ainda que logicamente essa separação exista, ela não está sendo claramente exibida no código.

1
2
3
4
5
6
7
8
9
10
11
12
describe Cart do
  it 'add itens' do
    product_a = create(:product, value: 10.0)
    product_b = create(:product, value: 15.0)
    item_a = create(:item, product: product_a, quantity: 1)
    item_b = create(:item, product: product_b, quantity: 2)
    cart.add_item(item_a)
    cart.add_item(item_b)
    expect(cart.total_itens).to eq(3)
    expect(cart.total_value).to eq(40.0)
  end
end

O código do teste acima poderia ser facilmente melhorado fazendo a aplicação da divisão das fases do teste, deste modo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
describe Cart do
  it 'add itens' do
    # Setup
    product_a = create(:product, value: 10.0)
    product_b = create(:product, value: 15.0)
    item_a = create(:item, product: product_a, quantity: 1)
    item_b = create(:item, product: product_b, quantity: 2)

    # Exercise
    cart.add_item(item_a)
    cart.add_item(item_b)

    # Verify
    expect(cart.total_itens).to eq(3)
    expect(cart.total_value).to eq(40.0)
  end
end

Mesmo que o teste esteja longe do ideal e as alterações tenham sido pequenas, o ganho de legibilidade é facilmente visto.

Imaginando um cenário real onde os arquivos de teste costumam ter um número de linhas similar ou por vezes até maior do que os arquivos de código de produção, legibilidade é um fator que pode ser decisivo na hora de realizar alguma alteração ou melhoria.

No exemplo dado não foi utilizada a etapa de Teardown porque não foi necessário e o próprio RSpec se encarrega de limpar os dados criados no teste.

Porém, em algumas situações é necessário seu uso, como no exemplo abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
describe FileManager do
  it 'check file' do
    # Setup
    file = File.open('file.txt', 'w')

    # Exercise
    file.write "Luiz Cezer"
    file.close

    # Verify
    expected = "Luiz Cezer"
    file_content = File.read('file.txt')
    expect(file_content).to eq(expected)

    # Teardown
    File.delete('file.txt')
  end
end

Em um cenário onde seriam testados valores escritos em arquivos, a etapa de Teardown pode ser utilizada para apagar o arquivo que foi criado durante o teste.

Describe e Context

O RSpec fornece estruturas para agrupar testes, utilizando os agrupadores describe e context. Entre os dois não há nenhuma diferença funcional, sendo que context é apenas um alias method para describe. A única diferença entre os dois é a forma conceitual de como devem ser aplicados.

A regra para esses agrupadores é:

  • Utilizar describe para descrever classes, módulos e métodos.
  • Utilizar context para descrever estados dos testes.
  • A descrição de um context não deve ser o mesmo do nome do método que será testado, para esse caso utilize describe.
  • Utilize context para explicar o motivo para o método ser executado.
1
2
3
4
5
6
7
8
9
10
11
describe Product do
  describe '#value' do
    context 'when product have discount' do
      # it ...
    end

    context 'when product is sold out' do
      # it ...
    end
  end
end

No exemplo acima, describe foi utilizado para descrever a classe a ser testada, que é Product e o método de instância value.

context foi utilizado para agrupar os testes dentro do contexto de um produto com desconto e um produto que já não está mais no estoque.

Subject e Let

Ao escrever testes é preciso identificar quem é o sujeito a ser testado e em certos casos quais são suas dependências. Para descrever quem é o sujeito do teste, o RSpec possui o método subject e para definir dependências é possível utilizar o método let. Outro ponto importante é que o subject é baseado na classe que está sendo utilizada no describe do teste.

É muito fácil confundir os dois, já que utilizando let também é possível criar o objeto a ser testado, porém com foco na legibilidade é preciso saber usar os dois métodos para o seu propósito correto.

No exemplo do teste abaixo é possível observar que o let está sendo usado para criar as dependências do teste, enquanto que o subject está sendo utilizado para criar a instância da classe a ser testada.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
describe Cart do
  let(:product_a) { create(:product, value: 10.0) }
  let(:product_b) { create(:product, value: 15.0) }
  let(:item_a) { create(:item, product: product_a, quantity: 1) }
  let(:item_b) { create(:item, product: product_b, quantity: 2) }
  subject(:cart) { Cart.new }

  before do
    cart.add_item(item_a)
    cart.add_item(item_b)
  end

  it { expect(cart.total_itens).to eq(2) }
  it { expect(cart.total_value).to eq(40.0) }
end

Ao definir uma variável fazendo uso de let ela não é imediatamente criada em memória, ou seja, let é lazy loading. Depois da primeira execução o valor da variável fica armazenado em cache até que o teste termine. Quando a variável for chamada novamente irá apenas utilizar esse valor armazenado sem precisar executar novamente.

Dicas Extras

  • Utilize apenas um it por teste.
  • O uso de before faz o código dentro do bloco ser executado antes de cada teste.
  • let é lazy loading, ou seja, a variável só é criada quando chamada pela primeira vez.
  • let! é executado na hora e já cria a variável.
  • Evite utilizar a palavra should na descrição do teste, ao invés disso, deixe claro o que o teste realmente deve fazer.
  • Não teste apenas o caminho feliz, teste também os casos que podem dar errado. Como o exemplo abaixo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
describe Calculator do
  describe '.sum' do
    # Testa o caminho feliz, se a soma é feita corretamente.
    it 'sum two numbers' do
      expect(Calculator.sum(2, 2)).to eq(4)
    end

    # Mas também testa um caso de erro, onde uma string é utiliza no lugar de um número.
    it 'raise error when receive string instead a number' do
      expect(Calculator.sum(2, 'number')).to raise_error
    end

    # E também testa um caso de erro, onde um valor nulo é utiliza no lugar de um número.
    it 'raise error when receive nil instead a number' do
      expect(Calculator.sum(2, nil)).to raise_error
    end
  end
end

Conclusão

A importância do código bem escrito e legível pode ser a diferença entre um projeto que irá ser escalável, performático, estável e um que não irá. Essa preocupação com qualidade também deve se estender a escrita dos testes, que devem receber tanta atenção quanto o código de produção.

Utilizar padrões de codificação de testes, seguir as melhores práticas e fazer correto uso das estruturas que a ferramenta fornece são um grande diferencial para ter testes bem escritos.

E você? Quais dicas e práticas utiliza para melhorar a legibilidade dos seus testes? Deixe aqui nos comentários.

Extras

Luiz Cezer Marrone Filho

Luiz Cezer Marrone Filho

Full Stack Developer

Comentários