Testes unitários e de integração
Quando e Porque
Resumo
Introdução
Testes unitários
Testes de integração
Desenvolvimento outside-in
Conclusão
O porque inicial...
Tudo muda o tempo todo
Testes unitários
Testando a menor unidade de códigopossível da forma mais isolada possível
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
Unitários: Código testado
class Jogador def initialize(objetivo) @objetivo = objetivo end def venceu? @objetivo.completo? endend
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
Unitários: Código testado
class Jogador def initialize(partida, objetivo) @objetivo = objetivo @partida = partida end def venceu? @objetivo.completo? self, @partida endend
Unitários: Ciclo do TDD
Crie um teste
Implemente a soluçãoRefatore
Unitários: Prós
Rápido de fazer e executar
Incentiva o baixo acoplamento
Facilita resolução de algoritmos
Unitários: Contras
Falsa sensação de terminado
Insegurança ao trocar contratos
Unitários: Exemplo de erro
class Objetivo def completo?(jogador, partida) def terminado?(jogador, partida) #... endend
Testes de integração
Testando para garantir que as classese componentes estejam se integrando
corretamente
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
Integração: Modulo de Objetivos
module Objetivos
class Conquistar24Territorios def self.completo?(jogador, partida) jogador.territorios.count >= 24 end end
end
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
Integração: Prós
Garante o funcionamento do sistema
Sensação de tarefa concluída
Integração: Contras
Testes mais lentos
Dificuldade de entender origem de erros
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
Desenvolvimento Outside-in
Definir caso de aceitação
Preparar teste end-to-end
Desenvolver funcionalidade com TDD
Repetir isso infinitamente
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
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
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
Acesso - funcionando
models/partida.rb:class Partida < ActiveRecord::Baseend
config/routes.rb:War::Application.routes.draw do resources :partidasend
controllers/partida_controller.rb:class PartidasController < ApplicationController def show endend
Partidas/show.html.erb:<h1>Ok</h1>
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
Primeira interação - funcionando
views/partidas.html.erb:<a href="#">brasil</a>
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
Mais interações - funcionando
views/partidas.html.erb:<form> <a href="#">brasil</a> <a href="#">argentina</a> <input name="Atacar" value="Atacar" type="submit"/></form>
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
Verificação - funcionando
views/partidas.html.erb:<form> <a href="#">brasil</a> <a href="#">argentina</a> <input name="Atacar" value="Atacar" type="submit"/> <div id="mensagem"> Territorio 'argentina' conquistado </div></form>
O que temos até agora?
Teste View Controller Model
Teste end-to-end ok
Hora de ir mais fundo
TDD a todo momento
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
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]) endend
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 %>
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(); });
O que temos até agora? (2)
Teste View Controller Model
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
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) endend
Teste end-to-end falha
$ rake spec:acceptanceFailures:
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!
Método não encontrado
class Partida < ActiveRecord::Base def executar_ataque(attrs) endend
$ tail -f log/test.logCompleted 500 Internal Server Error in 3msundefined method `executar_ataque' for #<Partida:0xa5549c0>
Um pouco de ousadia
Que tal começar um outro casode aceitação antes de terminar
este?
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
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
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) endend
Metodo burro pro teste passar
class Partida < ActiveRecord::Base def executar_ataque(attrs) ENV["forcar_vitoria"] != 'defesa' endend
O que temos até agora? (3)
Teste View Controller Model
Integração com os models
Dever de casa
Ciclo do outside-in
Crie um testeend-to-end
Refatore
Crie um teste
Implemente a soluçãoRefatore
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
Bibliografia
Test Driven Development: By Example
Kent Beck
Working Effectively with Legacy Code
Michael Feathers
Growing Object-Oriented Software, Guided by Tests
Steve Freeman
Mantenha contato
@timotta
http://programandosemcafeina.blogspot.com