Blog > Ruby in Tests (Parte 1): Testes de Unidade com RSpec

16/fev

Este post objetiva-se a demonstrar alguns conceitos relacionados à testes de unidade no contexto da linguagem Ruby, utilizando um framework bastante conhecido, chamado RSpec. Ao fim da leitura você será capaz de reconhecer os conceitos e escrever seu primeiro teste de unidade e em seguida construir uma classe e validar se ela atende ao teste.

Introdução

A prática de automação de testes traz uma série de benefícios ao ciclo de vida do desenvolvimento e teste de software, dentre os principais estão: a economia em tempo de execução dos testes, menos trabalho repetitivo e aumento na cobertura de testes perante o software.

Mike Cohn descreve em seu livro "Succeeding with Agile" a famosa pirâmide do teste, onde fica clara a destinação dos níveis de teste. O teste de unidade, nível do qual discutimos aqui neste post, testa a menor unidade de código de uma aplicação orientada a objetos, os métodos.

Para a criação dos testes de unidade é importante que alguns princípios de engenharia de software sejam seguidos na criação do código do software que será testado, entre eles, a alta coesão e o baixo acoplamento, que basicamente são, respectivamente, que cada classe tenha uma responsabilidade única (ex. a classe produto não deve gerenciar pedidos) e que uma classe não deve depender de outra para funcionar (ex. Para que o produto funcione a categoria deve estar funcionando).

Vemos então que a automação dos testes de unidade podem ajudar, mas que para isso o software também deve estar preparado para ser testado.

Apresentarei neste post alguns conceitos relacionados à testes de unidade no contexto da linguagem Ruby usando um framework denominado RSpec.

O que são testes de unidade?

O teste de unidade visa garantir que cada método faz o que ele deveria fazer, isso na perspectiva do código.

Vemos um exemplo abaixo:

class Avaliacao
	def avaliar(notaprova, notatrabalho)
		if (notaprova >= 7)
			if notatrabalho >= 7
				return "Aprovado"
			else
				return "Precisará repor a nota do trabalho"
			end
		else
			return "Terá de fazer DP"
		end
	end
end

Para este código teremos de fazer ao menos 3 testes:

notaprova = 7 e notatrabalho = 7 deve resultar em “Aprovado”

notaprova = 7 e notatrabalho = 6.9 deve resultar em “Precisará repor a nota do trabalho”

notaprova = 6.9 e notatrabalho = 0 deve resultar em “Terá de fazer DP”

Estes testes poderiam seriam feitos manualmente, mas teriamos um trabalho muito grande, uma vez que as aplicações geralmente possuem um nível de complexidade muito grande.

Automatizando testes de unidade

A automação dos testes de unidade ocorre através de utilização de frameworks, na maioria das vezes, de código aberto. Podemos destacar aqui algumas delas, RSpec para Ruby, JUnit para Java, NUnit para C#, PHPUnit para PHP, etc.

Como o contexto deste post é Ruby, na versão 2.1, será necessário baixar e instalar em seu computador.

Em Windows:

http://rubyinstaller.org

Em Linux:

apt-get install ruby

Obs. Ruby já vem instalado no Mac OS X.

O RSpec é o framework que usaremos neste post, e para facilitar a criação do projeto e utilização deste framework, iremos usar o Bundler, um gerenciador de dependências muito conhecido. Para isso, execute o comando abaixo:

gem install bundler

Agora vamos aos passos para preparação do projeto:

1. Crie um diretório chamado "Ruby in Tests" e dentro dele adicione o diretório chamado "Unit" (este será o diretório onde armazenaremos nosso projeto e seus testes de unidade);

2. Abra o prompt de comando e acesse o diretório Ruby in Tests;

3. Execute o comando bundler init e então um arquivo Gemfile será criado;

4. Adicione a linha a seguir logo abaixo do comando # gem rails e salve o arquivo:

   4.1. gem "rspec", "3.4.0"

5. Execute o comando bundler install

A partir de agora já é possível criar testes de unidade automatizados utilizando o RSpec.

A automação prevê que seja criados métodos de teste que consigam executar o trabalho manual, no exemplo usado no tópico "O que são testes de unidade?", usando RSpec, teríamos algo como:

describe Avaliacao do
  it "alunos com notas de prova e trabalho maior ou igual a 7 são aprovados" do
    # Arrange
    avaliacao = Avaliacao.new()
    # Act
    resultado = avaliacao.avaliar(7, 7)
    # Assert
    expect(resultado).to eq("Aprovado")
  end
  it "alunos com nota da prova maior ou igual a 7 e nota do trabalho menor que 7 precisarão repor a nota do trabalho" do
    # Arrange
    avaliacao = Avaliacao.new()
    # Act
    resultado = avaliacao.avaliar(7, 6.9)
    # Assert
    expect(resultado).to eq("Precisará repor a nota do trabalho")
  end
  it "alunos com notas de prova e trabalho menor do que 7 terão de fazer DP" do
    # Arrange
    avaliacao = Avaliacao.new()
    # Act
    resultado = avaliacao.avaliar(6.9, 0)
    # Assert
    expect(resultado).to eq("Terá de fazer DP")
  end
end

Este teste descreve os comportamentos da classe "Avaliacao". Os "it"s são métodos que avaliam cada comportamento da classe "Avaliacao". Vemos que ao fim de cada método existe expectativa, utilizada em conjunto com algum matcher. Nos exemplos acima, expectativas são representadas pelo método "expect" que avalia um determinado valor, no caso, o valor contido na variável resultado. Já o matcher, representado nos exemplos pelo método "eq" valida que o resultado é igual a um determinado valor, no caso "Aprovado", "Precisará repor a nota do trabalho" ou "Terá de fazer DP".

Expectativas podem ser negativas, veja um exemplo:

expect(resultado).not_to eq("Aprovado")

Métodos de teste de unidade podem utilizar a estrutura AAA (Arrange, Act e Assert), que divide cada método de teste em Preparação (Arrange), Ação (Act) e Asserção (Assert). Usar esta estrutura proporciona ao codificador a clareza na forma de criar um teste.

Uma vez escritos, os testes poderão ser executados automaticamente. Quando o método “avaliar" sofrer alterações e estas fizerem com que ele deixe de funcionar, após a execução dos testes estes falharão, dando um feedback imediato ao desenvolvedor.

Este é um outro grande benefício que adquirimos quando escrevemos testes de unidade automatizados: feedback instantâneo sobre áreas que funcionavam e deixaram de funcionar.

Para executar os testes que escrevemos, iremos salvar o arquivo como avaliacao_spec.rb dentro do diretório "Unit" e então executar o comando abaixo:

rspec Unit\avaliacao_spec.rb

Após a execução, veremos que a mensagem abaixo será apresentada:

avaliacao_spec.rb:1:in `<top (required)>': uninitialized constant Avaliacao (NameError)

Esta mensagem informa que o objeto "Avaliacao" ainda não está ao alcance dos testes, ou seja, é necessário requisitar a classe que será testada. Podemos fazer isto de diversas formas, mas usaremos uma bem simplória, veja abaixo:

require_relative "avaliacao"
describe Avaliacao
...
end

O método require_relative tem o objetivo de importar um arquivo, neste caso, "avaliacao.rb". Então, criaremos este arquivo e dentro dele iremos adicionar o código abaixo:

class Avaliacao
end

Iremos executar novamente os testes, mas agora, utilizaremos o parâmetro --color, para apresentar os resultados em cores, facilitando assim a compreensão do status dos testes:

rspec avaliacao_spec.rb --color

E então, teremos como resposta da execução, a seguinte mensagem:

NoMethodError:
  undefined method `avaliar' for #<Avaliacao:0x27d1dd8>

Além disso, serão apresentados três letras "F", demonstrando que os três testes falharam. Quando os testes passam, um "." é apresentado para mostrar que o teste passou.

Seguindo o conceito de TDD (Test-Driven Development), primeiro se constrói os testes para que em seguida inicie o desenvolvimento do software. Seguiremos este conceito, então, o próximo passo é criar o método "avaliar". Para isso, adicione o código abaixo dentro da classe Avaliacao:

def avaliar(notaprova, notatrabalho)
end

Ao executar novamente os testes, teremos três testes com falha, todos com uma mensagem semelhante a abaixo:

Failures:
  1) Avaliacao alunos com notas de prova e trabalho maior ou igual a 7 sao aprov
ados
     Failure/Error: expect(resultado).to eq("Aprovado")
       expected: "Aprovado"
            got: nil
       (compared using ==)
     # ./avaliacao_spec.rb:12:in `block (2 levels) in <top (required)>'

Isso ocorre porque a expectativa aguardava o texto "Aprovado", mas o método trouxe "nil", porque a funcionalidade ainda não foi implementada. Então vamos construir o código que faz com que o teste de aluno aprovado passe:

def avaliar(notaprova, notatrabalho)
   if notaprova >= 7
       if (notatrabalho >= 7)
               return "Aprovado"
          end
     end
end

Ao executar novamente os testes, teremos um cenário diferente. Serão exibidos um "." e dois "F", mostrando que o primeiro teste passou. O nosso próximo passou é implementar a lógica para fazer com que o segundo testes passe, então:

def avaliar(notaprova, notatrabalho)
   if notaprova >= 7
       if (notatrabalho >= 7)
               return "Aprovado"
       else
               return "Precisará repor a nota do trabalho"
          end
     end
end

A próxima execução revela dois testes executados com sucesso e apenas um teste que falhou. Agora vamos implementar o código que fará o último teste passar, veja:

def avaliar(notaprova, notatrabalho)
   if notaprova >= 7
       if (notatrabalho >= 7)
               return "Aprovado"
       else
               return "Precisará repor a nota do trabalho"
          end
   else
       return "Terá de fazer DP"
     end
end

E então, a execução dos testes revela o seguinte resultado:

...
Finished in 0.004 seconds (files took 0.179 seconds to load)
3 examples, 0 failures

Note que o tempo de execução é estremamente curto, de modo que podemos executar centenas ou milhares de testes em poucos segundos.

Estes testes serão executados constantemente, buscando avaliar que o código continua funcionando corretamente. Por exemplo, se alguém alterar o código de forma incorreta os próprios testes revelarão o problema e forçarão a correção do código, veja:

Altere a linha abaixo:

if notaprova >= 7

Por:

if notaprova > 7

Execute os testes e então teremos o resultado abaixo:

FF.
Failures:
  1) Avaliacao alunos com notas de prova e trabalho maior ou igual a 7 sao aprov
ados
     Failure/Error: expect(resultado).to eq("Aprovado")
       expected: "Aprovado"
            got: "Terá de fazer DP"
       (compared using ==)
     # ./avaliacao_spec.rb:12:in `block (2 levels) in <top (required)>'
  2) Avaliacao alunos com nota da prova maior ou igual a 7 e nota do trabalho me
nor que 7 precisarao repor a nota do trabalho
     Failure/Error: expect(resultado).to eq("Precisará repor a nota do trabalho
")
       expected: "Precisará repor a nota do trabalho"
            got: "Terá de fazer DP"
       (compared using ==)
     # ./avaliacao_spec.rb:23:in `block (2 levels) in <top (required)>'
Finished in 0.025 seconds (files took 0.183 seconds to load)
3 examples, 2 failures

Note que o próprio teste identificou o problema e então torna-se mais fácil e rápido dar manutenção no código.

Obs. Concerte o código para que os testes voltem a passar.

Hooks

Hooks são mecanismos que ajudam a controlar a execução dos métodos de teste, por exemplo, o que ocorre antes, durante ou depois de cada execução de método. Vemos abaixo uma implementação do hook before, que é executado antes de cada método "it":

describe Avaliacao do
  before do
     @avaliacao = Avaliacao.new()
  end
  it "alunos com notas de prova e trabalho maior ou igual a 7 são aprovados" do
    # Arrange
    # Act
    resultado = @avaliacao.avaliar(7, 7)
    # Assert
    expect(resultado).to eq("Aprovado")
  end
  it "alunos com nota da prova maior ou igual a 7 e nota do trabalho menor que 7 precisarão repor a nota do trabalho" do
    # Arrange
    # Act
    resultado = @avaliacao.avaliar(7, 6.9)
    # Assert
    expect(resultado).to eq("Precisará repor a nota do trabalho")
  end
  it "alunos com notas de prova e trabalho menor do que 7 terão de fazer DP" do
    # Arrange
    # Act
    resultado = @avaliacao.avaliar(6.9, 0)
    # Assert
    expect(resultado).to eq("Terá de fazer DP")
  end
end

Veja outros hooks na URL abaixo:

https://www.relishapp.com/rspec/rspec-core/v/2-2/docs/hooks

Matchers

Vimos exemplo do matcher "eq" usado para avaliar se o valor esperado é igual ao parâmetro passado para o matcher. Abaixo vemos uma lista com alguns matchers bastante utilizados.

Comparações:

expect(total).to be > 50
expect(total).to be >= 50
expect(total).to be <= 50
expect(total).to be <  50

Faixa de valores:

expect(valor).to be_between(10, 35).inclusive
expect(valor).to be_between(10, 35).exclusive

Análise de strings:

expect("Qualister").to start_with "Q"
expect("Qualister").to end_with "r"

Booleanos:

expect(true).to be_truthy    # Diferente de nil ou false
expect(true).to be true      # Igual a true
expect(false).to be_falsey    # Igual a nil ou false
expect(false).to be false     # Igual a false

Nil:

expect(nil).to be_nil

Conheça outros Matchers na URL abaixo:

https://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers

Versão final do código

require_relative "../avaliacao_class"
describe Avaliacao do
  it "alunos com notas de prova e trabalho maior ou igual a 7 são aprovados" do
    # Arrange
    avaliacao = Avaliacao.new()
    # Act
    resultado = avaliacao.avaliar(7, 7)
    # Assert
    expect(resultado).to eq("Aprovado")
  end
  it "alunos com nota da prova maior ou igual a 7 e nota do trabalho menor que 7 precisarão repor a nota do trabalho" do
    # Arrange
    avaliacao = Avaliacao.new()
    # Act
    resultado = avaliacao.avaliar(7, 6.9)
    # Assert
    expect(resultado).to eq("Precisará repor a nota do trabalho")
  end
  it "alunos com notas de prova e trabalho menor do que 7 terão de fazer DP" do
    # Arrange
    avaliacao = Avaliacao.new()
    # Act
    resultado = avaliacao.avaliar(6.9, 0)
    # Assert
    expect(resultado).to eq("Terá de fazer DP")
  end
end

Relatório de execução em HTML

Para gerar um relatório de execução dos testes no formato HTML basta adicionar os parâmetros --format html e --out arquivo_html.html, veja um exemplo:

rspec Unit\avaliacao_spec.rb --format html --out relatorio.html

A execução deste comando não traz retorno no prompt de comando, ele apenas gera o arquivo de relatório, semelhante a imagem abaixo:

Conclusão

Testes de unidade é uma prática utilizada na programação para avaliar se o código funciona conforme eperado. Pode-se fazer uso da prática TDD como forma de desenvolver seu código, pensando primeiro nos testes para depois pensar no código da aplicação. Utilizar um framework de testes de unidade auxilia desenvolvedores e testers a terem feedback mais veloz quanto ao funcionamento do código, tornando-o mais confiável.

Desafio Agile Testers

Este post é a resposta ao desafio feito pelo amigo Diego Blond ( @perdidonoteste).

POSTS RELACIONADOS

AGENDA

CURSOS RELACIONADOS