Este documento discute testes unitários e de integração no desenvolvimento de software. Ele explica que testes unitários testam unidades individuais de código de forma isolada, enquanto testes de integração verificam a integração entre classes e componentes. Além disso, o documento descreve a abordagem de desenvolvimento outside-in guiada por testes, na qual se começa com testes de aceitação de alto nível e se implementa funcionalidades iterativamente com TDD para atender esses testes.
4. Testes unitários
Testando a menor unidade de código
possível da forma mais isolada possível
5. Unitários: Exemplo inicial
describe Jogador do
let(:objetivo) { double }
subject { Jogador.new(objetivo) }
describe '#venceu?' do
it "deveria ser true se completou objetivo" do
objetivo.stub!(:completo?) { true }
subject.venceu?.should be
end
end
end
6. Unitários: Código testado
class Jogador
def initialize(objetivo)
@objetivo = objetivo
end
def venceu?
@objetivo.completo?
end
end
7. Unitários: Exemplo + completo
describe Jogador do
let(:objetivo) { double }
let(:partida) { double }
subject { Jogador.new(partida, objetivo) }
describe '#venceu?' do
#...
it "deveria informar partida e jogador ao objetivo" do
objetivo.should_receive(:completo?).with( subject, partida )
subject.venceu?
end
end
end
8. Unitários: Código testado
class Jogador
def initialize(partida, objetivo)
@objetivo = objetivo
@partida = partida
end
def venceu?
@objetivo.completo? self, @partida
end
end
10. Unitários: Prós
Rápido de fazer e executar
Incentiva o baixo acoplamento
Facilita resolução de algoritmos
11. Unitários: Contras
Falsa sensação de terminado
Insegurança ao trocar contratos
12. Unitários: Exemplo de erro
class Objetivo
def completo?(jogador, partida)
def terminado?(jogador, partida)
#...
end
end
13. Testes de integração
Testando para garantir que as classes
e componentes estejam se integrando
corretamente
14. Integração: Exemplo
describe Jogador, "com objetivo de conquistar 24 territorios" do
let(:objetivo) { Objetivos::Conquistar24Territorios }
let(:partida) { Partida.new }
subject { Jogador.create(partida: partida, objetivo: objetivo) }
it "deveria vencer se tiver 24 territorios" do
24.times { subject.territorios << Territorio.new }
subject.venceu?.should be
end
end
15. Integração: Modulo de Objetivos
module Objetivos
class Conquistar24Territorios
def self.completo?(jogador, partida)
jogador.territorios.count >= 24
end
end
end
16. Integração: End to end
Comportamento do ponto de vista do usuário
Exemplo:
Dado que o jogador tem 23 territórios
E o objetivo dele é conquistar 24 territórios
Quando o jogador conquistar 1 território
E finalizar a rodada
Então Jogador vence a partida
17. Integração: Prós
Garante o funcionamento do sistema
Sensação de tarefa concluída
18. Integração: Contras
Testes mais lentos
Dificuldade de entender origem de erros
19. Como unir?
Sensação de finalizado e segurança dos testes
de integração end to end
Facilidade, rapidez e desacoplamento
proporcionados pelos testes unitários
20. Desenvolvimento Outside-in
Definir caso de aceitação
Preparar teste end-to-end
Desenvolver funcionalidade com TDD
Repetir isso infinitamente
21. Outside-in: Caso de aceitação
Jogador com 4 exércitos em um território
ataca território vizinho que possui apenas
1 exército e o conquista
22. Setup – inicialmente fake
feature "Atacar" do
scenario "territorio vizinho com 3 dados e conquistar" do
dado_jogador_com_exercitos_no_pais(4, :brasil)
dado_jogador_com_exercitos_no_pais(1, :argentina)
end
private
def dado_jogador_com_exercitos_no_pais(exercitos, pais)
end
end
23. Acesso - falhando
feature "Atacar" do
before :each do
@partida = Partida.create
end
scenario "territorio vizinho com 3 dados e conquistar" do
jogador1 = dado_jogador_com_exercitos_no_pais(4, :brasil)
jogador2 = dado_jogador_com_exercitos_no_pais(1, :argentina)
dado_que_jogador_esta_logado(jogador1)
end
#...
def dado_que_jogador_esta_logado(jogador)
visit partida_path(@partida)
end
end
24. Acesso - funcionando
models/partida.rb:
class Partida < ActiveRecord::Base
end
config/routes.rb:
War::Application.routes.draw do
resources :partidas
end
controllers/partida_controller.rb:
class PartidasController < ApplicationController
def show
end
end
Partidas/show.html.erb:
<h1>Ok</h1>
25. Primeira interação - falhando
scenario "territorio vizinho com 3 dados e conquistar" do
#...
dado_que_jogador_esta_logado(jogador1)
quando_selecionar_territorio_que_vai_atacar(:brasil)
end
private
#...
def quando_selecionar_territorio_que_vai_atacar(pais)
click_link pais.to_s
end
26. Primeira interação - funcionando
views/partidas.html.erb:
<a href="#">brasil</a>
27. Mais interações - falhando
scenario "territorio vizinho com 3 dados e conquistar" do
#...
quando_selecionar_territorio_que_vai_atacar(:brasil)
quando_selecionar_territorio_atacado(:argentina)
quando_confirmar_ataque
end
#...
def quando_selecionar_territorio_atacado(pais)
click_link pais.to_s
end
def quando_confirmar_ataque
click_button 'Atacar'
end
29. Verficação - falhando
scenario "territorio vizinho com 3 dados e conquistar" do
#...
quando_confirmar_ataque
entao_territorio_eh_conquistado(:argentina)
end
#...
def entao_territorio_eh_conquistado(pais)
find('#mensagem').text.strip
.should == "Territorio '#{pais}' conquistado"
end
33. Teste unitário do controller
describe AtaquesController, 'POST' do
it 'deveria redirecionar para a partida' do
post :create, partida_id: 1, ataque: {}
response.should redirect_to(partida_path(1))
end
it 'deveria colocar mensagem de sucesso' do
post :create, partida_id: 1, ataque: { pais_atacado: 'argentina' }
flash[:mensagem].
should == "Territorio 'argentina' conquistado"
end
end
34. Controller de ataque
class AtaquesController < ApplicationController
def create
pais = params[:ataque][:pais_atacado]
flash[:mensagem] = "Territorio #{pais}' conquistado"
redirect_to partida_path(params[:partida_id])
end
end
35. View tem que mudar
<div id="mensagem"><%= flash[:mensagem] %></div>
<%= form_for :ataque,
url: partida_ataques_url(@partida),
method: :post do |f| %>
<a href="#">brasil</a>
<a href="#">argentina</a>
<%= f.hidden_field 'pais_que_ataca' %>
<%= f.hidden_field 'pais_atacado' %>
<%= f.submit value: 'Atacar' %>
<% end %>
36. Javascript incluído
var paisQueAtaca = $('#ataque_pais_que_ataca');
var paisAtacado = $('#ataque_pais_atacado');
var paisAtual = paisQueAtaca;
var marcar = function(texto) {
paisAtual.val(texto);
}
var trocaPaisAtual = function() {
paisAtual = paisAtual == paisQueAtaca ?
paisAtacado : paisQueAtaca;
}
$('a').click(function(e){
e.preventDefault();
marcar($(this).text());
trocaPaisAtual();
});
37. O que temos até agora? (2)
Teste View Controller Model
38. Teste unitário do controller
describe AtaquesController, 'POST' do
let(:partida) { double(id: 1, executar_ataque: true) }
before :each do
Partida.stub!(:find) { partida }
end
#...
it 'deveria executar ataque na partida' do
params_ataque = {"meu_ataque" => true}
partida.should_receive(:executar_ataque).with(params_ataque)
post :create, partida_id: 1, ataque: params_ataque
end
end
39. Controller de ataque
class AtaquesController < ApplicationController
def create
ataque = params[:ataque]
partida = Partida.find params[:partida_id]
partida.executar_ataque(ataque)
pais = ataque[:pais_atacado]
flash[:mensagem] = "Territorio '#{pais}' conquistado"
redirect_to partida_path(partida)
end
end
40. Teste end-to-end falha
$ rake spec:acceptance
Failures:
1) Atacar territorio vizinho com 3 dados e conquistar
Failure/Error: find('#mensagem').text.strip.should == "Territorio...
Capybara::ElementNotFound:
Unable to find css "#mensagem"
Erro dificil de encontrar a origem!
41. Método não encontrado
$ tail -f log/test.log
Completed 500 Internal Server Error in 3ms
undefined method `executar_ataque' for #<Partida:0xa5549c0>
class Partida < ActiveRecord::Base
def executar_ataque(attrs)
end
end
42. Um pouco de ousadia
Que tal começar um outro caso
de aceitação antes de terminar
este?
43. Caso da derrota
scenario "territorio vizinho com 3 dados e conquistar", js: true do
dado_que_dados_vermelhos_estao_sortudos
#...
end
scenario "territorio vizinho com 3 dados e nao conquistar", js: true do
dado_que_dados_vermelhos_estao_azarentos
#...
end
def dado_que_dados_vermelhos_estao_azarentos
ENV["forcar_vitoria"] = "defesa"
end
def dados_que_dados_vermelhos_estao_sortudos
ENV["forcar_vitoria"] = "ataque"
end
44. Teste do controller
context 'conquistando' do
before :each do
partida.stub!(:executar_ataque) { true }
end
it 'deveria colocar mensagem de sucesso' do
post :create, partida_id: 1, ataque: { pais_atacado: 'argentina' }
flash[:mensagem].should == "Territorio 'argentina' conquistado"
end
end
context 'nao conquistando' do
before :each do
partida.stub!(:executar_ataque) { false }
end
it 'deveria colocar mensagem de insucesso' do
post :create, partida_id: 1, ataque: { pais_atacado: 'argentina' }
flash[:mensagem].should == "Territorio 'argentina' nao foi conquistado"
end
end
45. Controller de ataque
class AtaquesController < ApplicationController
def create
ataque = params[:ataque]
partida = Partida.find params[:partida_id]
pais = ataque[:pais_atacado]
if partida.executar_ataque(ataque)
flash[:mensagem] = "Territorio '#{pais}' conquistado"
else
flash[:mensagem] = "Territorio '#{pais}' nao foi conquistado"
end
redirect_to partida_path(partida)
end
end
46. Metodo burro pro teste passar
class Partida < ActiveRecord::Base
def executar_ataque(attrs)
ENV["forcar_vitoria"] != 'defesa'
end
end
47. O que temos até agora? (3)
Teste View Controller Model
49. Ciclo do outside-in
Crie um teste
end-to-end
Crie um teste
Refatore
Refatore Implemente a solução
50. Conclusão
Sempre guie o desenvolvimento por testes
Tanto por testes de integração como unitários
Combine-os em uma estratégia outside-in
Siga sempre o mantra dos pequenos passos
51. Bibliografia
Test Driven Development: By Example
Kent Beck
Working Effectively with Legacy Code
Michael Feathers
Growing Object-Oriented Software, Guided by Tests
Steve Freeman