Testes unitários e de integração: Quando e Porque

Preview:

DESCRIPTION

Palestra apresentada no primeiro ENCATEC

Citation preview

   

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@gmail.com

@timotta

http://programandosemcafeina.blogspot.com

Recommended