Go to English Blog

Começando com testes automatizados no Ruby

Leia em 11 minutos

Se você perguntar para um desenvolvedor mais experiente quais são as coisas essenciais que todo desenvolvedor precisa saber, esta lista provavelmente terá “testes automatizados”. E não é para menos.

Testes automatizados tem como objetivo dar a confiança necessária para modificar o seu código. Seu objetivo principal não é prevenir erros; eles sempre existirão1. Em vez disso, eles permitem que você modifique seu código com a confiança de que você não introduziu nenhum efeito colateral, seja ao modificar uma funcionalidade existente, seja ao adicionar uma nova funcionalidade.

Mesmo sendo uma técnica extremamente importante, nem todo desenvolvedor sabe como fazer isso. Eu mesmo só comecei a escrever testes automatizados depois que comecei a trabalhar com Ruby, 6 anos depois de começar minha carreira profissional. E não é porque escrever testes seja algo muito difícil; é porque não sabemos como fazê-lo!

A ideia deste artigo é dar o conhecimento necessário para que você comece a escrever testes automatizados. Não iremos falar sobre tudo o que a ferramenta tem a oferecer, muito menos explicar sobre técnicas mais avançadas de teste. Por isso, se você já um desenvolvedor mais experiente que já escreve testes automatizados, este artigo não é para você.

Por que escrever testes automatizados?

Bem, como já disse, o principal motivo é dar-lhe confiança para modificar o seu código. Mas este não é o último motivo.

Existem muitos outros motivos para escrever testes, mas se a lista acima ainda não é suficiente para convencer você, saia daqui. Sério, vá procurar outra coisa para ler; eu espero antes de continuar.

Sobre os tipos de testes

Se você ainda está lendo este artigo é porque se importa com a qualidade de seu código e quer se tornar um desenvolvedor profissional. Que bom!

Pense sobre como você programa hoje, sem testes automatizados. Primeiro você escreve a funcionalidade (por completo ou parcialmente), e tenta executar no contexto de seu projeto; se for uma ferramenta de linha de comando, irá executar o comando, fazendo-o funcionar na base da tentativa e erro. Se for um projeto web, irá preencher um formulário e enviar estes dados até que eles sejam salvos no banco de dados, também na base da tentativa e erro.

A ideia do teste manual é garantir que dada uma ação, o resultado é algo que você espera. De uma forma simplista, o teste automatizado só irá realizar estes processos com código para que você não tenha que fazer isso manualmente.

Para atingir o objetivo de validação, você pode utilizar diferentes tipos de testes:

E estes são apenas alguns exemplos. Existe uma infinidade de tipos de testes e você não precisa necessariamente escrever todos eles. Tudo depende do valor que os testes trarão para o seu projeto. Afinal, testes também precisam ser mantidos.

Neste artigo vamos ver como funciona o workflow do Test-Driven Development, além de escrever alguns testes unitários usando Minitest, o framework de testes oficial do Ruby. No entanto, saiba que você pode escrever testes automatizados sem seguir necessariamente o workflow de TDD, e isso não significa que você esteja fazendo errado.

Entendendo o Test-Driven Development

Workflow Red-Green-Refactor

Test-Driven Development2 é uma técnica que se baseia na repetição de ciclos curtos de desenvolvimento. Primeiro, o desenvolvedor escreve um teste automatizado que define o comportamento da nova funcionalidade. Então, o desenvolvedor escreve a menor quantidade de código necessária para fazer este teste passar. Finalmente, o desenvolvedor pode refatorar o código para níveis de qualidade aceitáveis3.

Etapa 1 — Adicione um teste

No ciclo de TDD, toda funcionalidade começa pelo teste. Para escrever este teste, o desenvolvedor precisa entender claramente os requisitos da tarefa. É aqui a maior diferença entre escrever o teste antes ou depois do código; O TDD força o desenvolvedor a entender os requisitos antes de escrever o código.

Etapa 2 — Execute os testes e veja se o novo teste falha

Após adicionar o novo teste, você deve executar todos os testes do seu sistema, para garantir que a funcionalidade ainda não existe e para garantir que o teste não passou por engano; talvez porque você tenha escrito o teste de forma equivocada ou o teste simplesmente não faz sentido em existir. Esta etapa aumenta a confiança de que o teste passa apenas nos casos desejados.

Etapa 3 — Escreva o código

A próxima etapa é escrever o código, fazendo com que o teste passe. Você não precisa escrever o melhor código que já existiu e totalmente aceitável que o código seja escrito de uma forma deselegante, sem preocupação com a performance ou outro requisito.

Lembre-se que o objetivo desta etapa é fazer com que o teste passe; por isso, nada de adicionar funcionalidades extras por qualquer que seja o motivo.

Etapa 4 — Execute os testes

Execute novamente os testes. Se eles passarem, o desenvolvedor tem a confiança de que os requisitos foram atingidos e que não quebra ou degrada as funcionalidades existentes. Se os testes não passarem, ajuste o código que você escreveu até que isso seja verdade.

Etapa 5 — Refatore o código

Para que a qualidade de seu código seja mantida (ou aumente), é necessário que você refatore o código do projeto constantemente. Altere a organização de arquivos, reduza a duplicação de lógica, faça com que nomes de classes, variáveis e métodos indiquem claramente sua intenção e propósito. Se os métodos forem muito grandes, extraia para outros métodos ou classes, tornando sua responsabilidade menor, mas melhor definida.

Para cada modificação que você fizer, execute novamente os testes. Assim você terá a confiança de que as alterações não afetaram o comportamento do projeto.

Um exemplo prático

Para mostrar como escrever testes automatizados vamos implementar um conversor de temperaturas. Antes de mais nada, instale a gem minitest.

$ gem install minitest

Depois, crie um diretório chamado temperature; crie também o arquivo temperature.rb, que terá o código de nossa implementação. Crie também um arquivo test/temperature_test.rb, que terá nossos testes automatizados; por enquanto, adicione apenas a estrutura de nossa suíte de testes.

require 'minitest/autorun'
require_relative './temperature'

class TemperatureTest < Minitest::Test
end

No Minitest utilizamos classes para agrupar diversos testes. E cada teste é um método que começa com o nome test_. Vamos criar um método que valida se o número um é igual a ele mesmo.

require 'minitest/autorun'
require_relative './temperature'

class TemperatureTest < Minitest::Test
  def test_one_is_one
    assert_equal 1, 1
  end
end

Para fazer esta suíte ser executada, utilize o comando ruby test/temperature_test.rb.

$ ruby temperature_test.rb
Run options: --seed 1078

# Running:

.

Finished in 0.001004s, 995.9376 runs/s, 995.9376 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Voltando ao nosso teste, perceba que estamos utilizando um método chamado assert_equal. Este é um método de asserção, que irá validar se o nosso código está funcionando corretamente; neste caso, 1 é sempre igual a 1. O Minitest possui muitos métodos de asserção, incluindo métodos com semântica negativa. Para ver uma lista completa, acesse a documentação.

Nosso teste não faz muita coisa. Por isso, vamos definir os requisitos do nosso conversor de temperaturas.

  1. É possível fazer a conversão entre Fahrenheit, Celsius e Kelvin.
  2. As fórmulas de conversão são:
    • Celsius → Fahrenheit: (C * 9/5) + 32
    • Celsius → Kelvin: C + 273.15
    • Fahrenheit → Celsius: (F - 32) * 5/9
    • Fahrenheit → Kelvin: (F - 32) * 5/9 + 273.15
    • Kelvin → Fahrenheit: (K - 273.15) * 9/5 + 32
    • Kelvin → Celsius: K - 273.15

Vamos escrever nosso primeiro teste, garantindo a conversão de Celsius para Fahrenheit.

require 'minitest/autorun'
require_relative './temp_converter'

class TemperatureTest < Minitest::Test
  def test_convert_from_celsius_to_fahrenheit
    assert_equal 98.6, Temperature.new(37, 'celsius').to_fahrenheit
  end
end

Execute os testes; como ainda não definimos a classe Temperature, esse é o erro que será lançado.

$ ruby temperature_test.rb
  ...
  1) Error:
TemperatureTest#test_convert_from_celsius_to_fahrenheit:
NameError: uninitialized constant TemperatureTest::Temperature
    temperature_test.rb:6:in `test_convert_from_celsius_to_fahrenheit'

Para resolver este problema basta definir a classe Temperature no arquivo temperature.rb.

class Temperature
end

Execute novamente os testes.

  1) Error:
TemperatureTest#test_convert_from_celsius_to_fahrenheit:
ArgumentError: wrong number of arguments (2 for 0)
    temperature_test.rb:6:in `initialize'
    temperature_test.rb:6:in `new'
    temperature_test.rb:6:in `test_convert_from_celsius_to_fahrenheit'

Agora, nosso erro acontece porque estamos passando dois parâmetros na inicialização do objeto, ao passo que a classe Temperature não espera nenhum.

class Temperature
  def initialize(value, unit)
  end
end

Executar os testes manualmente é uma tarefa um tanto quanto chata. Você pode, alternativamente, usar projetos como minitest-autotest ou Guard; eles irão detectar as modificações nos arquivos e executar automaticamente os testes4. De qualquer modo, este é o erro neste momento:

  1) Error:
TemperatureTest#test_convert_from_celsius_to_fahrenheit:
NoMethodError: undefined method `to_fahrenheit' for #<Temperature:0x007f9a5424cb10>
    temperature_test.rb:6:in `test_convert_from_celsius_to_fahrenheit'

Agora podemos definir o método Temperature#to_fahrenheit.

class Temperature
  def initialize(value, unit)
  end

  def to_fahrenheit
  end
end

E agora, em vez de lançar um erro, o teste irá falhar.

  1) Failure:
TemperatureTest#test_convert_from_celsius_to_fahrenheit [temperature_test.rb:6]:
Expected: 98.6
  Actual: nil

Aplique a fórmula que converte de Celsius para Fahrenheit.

class Temperature
  def initialize(value, unit)
    @value = value
    @unit = unit
  end

  def to_fahrenheit
    @value * (9 / 5.0) + 32
  end
end

Ao executar os testes mais uma vez eles… falham?

  1) Failure:
TemperatureTest#test_convert_from_celsius_to_fahrenheit [temperature_test.rb:6]:
Expected: 98.6
  Actual: 98.60000000000001

Dado o modo como o Ruby faz a implementação de pontos flutuantes5, temos um problema de arredondamento. Em vez de ter que lidar com isso em nosso código, podemos utilizar um outro método de asserção; trata-se de assert_in_delta, feito especialmente para comparar floats.

require 'minitest/autorun'
require_relative './temperature'

class TemperatureTest < Minitest::Test
  def test_convert_from_celsius_to_fahrenheit
    assert_in_delta 98.6, Temperature.new(37, 'celsius').to_fahrenheit
  end
end

Se você executar os testes verá que agora eles estão passando.

Vamos escrever mais um teste, desta vez garantindo que a conversão de Fahrenheit para Celsius funciona.

require 'minitest/autorun'
require_relative './temperature'

class TemperatureTest < Minitest::Test
  def test_convert_from_celsius_to_fahrenheit
    assert_in_delta 98.6, Temperature.new(37, 'celsius').to_fahrenheit
  end

  def test_convert_from_fahrenheit_to_celsius
    assert_in_delta 37, Temperature.new(98.6, 'fahrenheit').to_celsius
  end
end
  1) Error:
TemperatureTest#test_convert_from_fahrenheit_to_celsius:
NoMethodError: undefined method `to_celsius' for #<Temperature:0x007f940487cbf8 @value=98.6, @unit="fahrenheit">
    temperature_test.rb:10:in `test_convert_from_fahrenheit_to_celsius'

A implementação do método Temperature#to_celsius será algo como isso:

class Temperature
  def initialize(value, unit)
    @value = value
    @unit = unit
  end

  def to_fahrenheit
    @value * (9 / 5.0) + 32
  end

  def to_celsius
    (@value - 32) * 5 / 9
  end
end

Os testes nos dizem que está tudo certo. No entanto, nossa implementação ainda está incompleta. Não estamos convertendo para Kelvin.

require 'minitest/autorun'
require_relative './temperature'

class TemperatureTest < Minitest::Test
  # ...

  def test_convert_from_celsius_to_kelvin
    assert_equal 310.15, Temperature.new(37, 'celsius').to_kelvin
  end
end

A implementação é bem simples.

class Temperature
  # ...

  def to_kelvin
    @value + 273.15
  end
end

Finalmente, vamos fazer a conversão de Kelvin para Celsius.

require 'minitest/autorun'
require_relative './temperature'

class TemperatureTest < Minitest::Test
  # ...

  def test_convert_from_kelvin_to_celsius
    assert_equal 37, Temperature.new(310.15, 'kelvin').to_celsius
  end
end

Ao executar o teste teremos um valor completamente errado.

  1) Failure:
TemperatureTest#test_convert_from_kelvin_to_celsius [temperature_test.rb:18]:
Expected: 37
  Actual: 154.52777777777777

Em vez de utilizar a fórmula correta, foi utilizada a fórmula de Fahrenheit para Celsius. Vamos ter que alterar o método Temperature#to_celsius.

class Temperature
  def initialize(value, unit)
    @value = value
    @unit = unit
  end

  def to_fahrenheit
    @value * (9 / 5.0) + 32
  end

  def to_celsius
    case @unit
    when 'fahrenheit'
      (@value - 32) * 5 / 9
    when 'kelvin'
      @value - 273.15
    end
  end

  def to_kelvin
    @value + 273.15
  end
end

Finalmente, vamos adicionar testes para garantir a conversão entre Fahrenheit e Kelvin.

require 'minitest/autorun'
require_relative './temperature'

class TemperatureTest < Minitest::Test
  # ...

  def test_convert_from_fahrenheit_to_kelvin
    assert_equal 310.15, Temperature.new(98.6, 'fahrenheit').to_kelvin
  end

  def test_convert_from_kelvin_to_fahrenheit
    assert_equal 98.6, Temperature.new(310.15, 'kelvin').to_fahrenheit
  end
end
  1) Failure:
TemperatureTest#test_convert_from_fahrenheit_to_kelvin [temperature_test.rb:22]:
Expected: 310.15
  Actual: 371.75


  2) Failure:
TemperatureTest#test_convert_from_kelvin_to_fahrenheit [temperature_test.rb:26]:
Expected |98.6 - 590.27| (491.66999999999996) to be <= 0.001.

Para que isso funcione, precisamos garantir que a conversão é sempre feita com os valores em Celsius.

class Temperature
  def initialize(value, unit)
    @value = value
    @unit = unit
  end

  def to_fahrenheit
    to_celsius * (9 / 5.0) + 32
  end

  def to_celsius
    case @unit
    when 'fahrenheit'
      (@value - 32) * 5 / 9.0
    when 'kelvin'
      @value - 273.15
    when 'celsius'
      @value
    end
  end

  def to_kelvin
    to_celsius + 273.15
  end
end

E agora, todos os testes passam.

$ ruby temperature_test.rb
Run options: --seed 9136

# Running:

......

Finished in 0.001389s, 4318.1342 runs/s, 4318.1342 assertions/s.

6 runs, 6 assertions, 0 failures, 0 errors, 0 skips

Agora, podemos refatorar nosso código. Embora ele já seja extremamente simples e fácil de entender, tem uma coisa que podemos modificar. Em vez de passar as unidades de temperatura como strings, podemos usar constantes.

class Temperature
  C = 'celsius'.freeze
  F = 'fahrenheit'.freeze
  K = 'kelvin'.freeze

  def initialize(value, unit)
    @value = value
    @unit = unit
  end

  def to_fahrenheit
    to_celsius * (9 / 5.0) + 32
  end

  def to_celsius
    case @unit
    when F
      (@value - 32) * 5 / 9.0
    when K
      @value - 273.15
    when C
      @value
    end
  end

  def to_kelvin
    to_celsius + 273.15
  end
end

Podemos mudar nossos testes também para utilizar estas constantes.

require 'minitest/autorun'
require_relative './temperature'

class TemperatureTest < Minitest::Test
  def test_convert_from_celsius_to_fahrenheit
    assert_in_delta 98.6, Temperature.new(37, Temperature::C).to_fahrenheit
  end

  def test_convert_from_fahrenheit_to_celsius
    assert_in_delta 37, Temperature.new(98.6, Temperature::F).to_celsius
  end

  def test_convert_from_celsius_to_kelvin
    assert_equal 310.15, Temperature.new(37, Temperature::C).to_kelvin
  end

  def test_convert_from_kelvin_to_celsius
    assert_equal 37, Temperature.new(310.15, Temperature::K).to_celsius
  end

  def test_convert_from_fahrenheit_to_kelvin
    assert_equal 310.15, Temperature.new(98.6, Temperature::F).to_kelvin
  end

  def test_convert_from_kelvin_to_fahrenheit
    assert_in_delta 98.6, Temperature.new(310.15, Temperature::K).to_fahrenheit
  end
end

Os testes confirmam que estas modificações não alteraram o comportamento de nosso código.

Finalizando

Embora pareça ser uma técnica simples, escrever testes automatizados pode ser bem complexo, principalmente se a complexidade de seu código for alta. Mas não adianta dar exemplos aqui; nada como a experiência do mundo real. A dica mais importante que posso dar é ouça o que seus testes estão dizendo. Se é muito difícil testar, é porque a complexidade de seu código está muito alta e a interação entre os objetos pode ser melhorada.

Para saber mais sobre testes, veja os links abaixo: são livros, artigos e vídeos que podem dar um pouco mais de contexto sobre o assunto.


  1. Embora não seja o objetivo principal, isso pode ser uma consequência. 

  2. Este processo também é conhecido como Red-Green-Refactor e deve ser feito continuamente sempre que você precisar modificar o sistema que está sendo testado. 

  3. Pessoalmente, não sigo o TDD sempre à risca; muitas vezes escrevo o código antes, para ter mais conhecimento sobre o que preciso fazer. Eu costumava me sentir mal por isso, mas não estou sozinho nesse pensamento. E se este também é o seu caso, não se sinta mal; utilize o workflow que você acha que funciona melhor para você. 

  4. Talvez seja preciso ter algumas configurações extras que não vou mostrar aqui porque foge do escopo do exemplo. 

  5. O Ruby utiliza a implementação do padrão IEE-754, que pode ter problemas de arredondamento. Este “erro” também acontece em outras linguagens que utilizam o mesmo padrão, como Python e JavaScript.