Lições Aprendidas sobre Testes Danilo Sato Rails Summit Latin America www.dtsato.com 16/Out/2008
Um pouco sobre vocês Será que estou na palestra certa?
Minha história com testes automatizados
Era uma vez
Era uma vez printf("passou 1\n"); printf("valor: %s\n",!
Era uma vez printf("passou 1\n"); printf("valor: %s\n",!
xunit! XP Kent Beck! Automatizar passos manuais! Você não quebra o que já está funcionando
Test::Unit! Classe que estende Test::Unit::TestCase! Métodos devem começar com "test"! Asserções:! assert(bool)!! assert_equal(expected, actual)!! assert_raise(args, blk)!! assert_nil(actual)!!!
require 'rubygems'" require 'test/unit'" require 'lib/card'" class CardTest < Test::Unit::TestCase" def test_equal" assert Card.new('QS') == Card.new('QC')" end" def test_greater" assert Card.new('AS') > Card.new('KH')" end" def test_lower" assert Card.new('TD') < Card.new('JH')" end" end
StrutsTestCase + AspectJ! Gravador de testes!! Gera as classes de teste pra você!
Primeiros Erros! Testes imensos! Várias asserções no mesmo cenário! Erros não ajudam muito! Debuggar o teste
Primeiras Lições! Testes grandes são difíceis de manter! Mas é bom cobrir a funcionalidade do ponto de vista externo Código de teste também é Código
! RCov! Quais linhas estão sendo executadas pelos testes?! Objetivo: 100% Cobertura de Código
Cuidado!! Cobertura não garante qualidade dos testes!! Heckle: Mutação
! Arrange-Act-Assert Testes Pequenos! Single Assertion per Test
E mais problemas! setup enormes! Carrega e limpa o banco de dados! E os métodos estáticos?
Test-Driven Development
Test-Driven Development
Testes Ajudam no Design! Refatoração! Maus Cheiros! YAGNI! Design Evolutivo
Testes Comunicam Intenção! Documentação Executável! Legibilidade é importante! Lemos mais código do que escrevemos
require 'rubygems'" require 'test/unit'" require 'lib/card'" class CardTest < Test::Unit::TestCase" def test_equal" assert Card.new('QS') == Card.new('QC')" end" def test_greater" assert Card.new('AS') > Card.new('KH')" end" def test_lower" assert Card.new('TD') < Card.new('JH')" end" end
RSpec require 'rubygems'" require 'spec'" require 'lib/card'" describe Card do" it 'should compare based on rank' do" Card.new('AS').should > Card.new('KH')" Card.new('QS').should == Card.new('QC')" Card.new('TD').should < Card.new('JH')" end" end!
Mais algumas lições Se está difícil de testar, provavelmente seu design precisa evoluir Use o código de teste para comunicar suas intenções atuais para o você do futuro (e seus colegas também)
Testes Unitários! Integridade Interna! Rápidos! Independentes! Desacoplados! Escrito por e para desenvolvedores! Não indicam integridade externa
Visão Mais Ampla
Histórias! Como <papel/usuário>! Eu gostaria de <funcionalidade>! Pois <valor de negócio>
Cenários (Exemplos)! Dado <contexto>! Quando <evento>! Então <consequência>
Story: I can rank poker hands" As a game player" I want to rank a poker hand" So that I can decide a winner for the prize" Scenario: Straight flush wins Four of a kind" Given a black hand with cards: 2H 3H 4H 5H 6H" And a white hand with cards: AC AH AD AS KC" Then black should win" Scenario: Four of a kind wins Full house" Given a white hand with cards: 2C 2H 2D 2S AC" And a black hand with cards: AC AH AD KS KC" Then white should win" Scenario: Full house wins Flush" Given a black hand with cards: 2C 2H 2S 3C 3S" And a white hand with cards: 4C 8C TC KC AC" Then black should win
require 'rubygems'" require 'spec/story'" require 'lib/hand'" require 'lib/card'" steps_for :poker do" Given "a $color hand with cards: $cards" do color, cards " instance_eval "@#{color} = Hand.new('#{cards}')"" end" Then ("black should win") { @black.should > @white }" Then ("white should win") { @white.should > @black }" Then ("tie") { @black.should == @white }" end" with_steps_for :poker do" Dir["**/*.story"].each { file run file }" end
! Novo Story Runner Cucumber! Histórias em Português! Funcionalidade: Adição! Como um péssimo matemático" Eu quero saber saomr dois números! Para evitar erros bobos" Cenário: Adicionar dois números! Dado que eu digitei 50 na calculadora" E que eu digitei 70 na calculadora" Quando eu aperto o botão de soma! Então o resultado na calculadora deve ser 120
require 'spec " class Calculadora" def push(n)" @args = []" @args << n" end " def soma; @args.inject(0) { n,sum sum+n}; end" end" Before { @calc = Calculadora.new }" Given /que eu digitei (\d+) na calculadora/ do n! @calc.push n.to_i! end " When 'eu aperto o botão de soma do! @result = @calc.soma! end " Then(/o resultado na calculadora deve ser (\d*)/) { result @result.should == result.to_i }
Testes de Aceitação! Integridade Externa! Mais lentos! Acoplados! Mais difícil detectar causa de erros! Escrito para clientes! Não indicam integridade interna
! Dirige o browser como se um usuário estivesse navegando as páginas! Várias ferramentas:! Selenium IDE! Selenium RC! Selenium on Rails! Selenium Grid* Selenium * Você está curioso? Vá para a palestra do lado!
Testes Unitários
Testes de Aceitação
?
Integração
Mock! Permitem especificar interações! Trocam o objeto real por um dublê! Verificam se as expectativas foram atendidas! Substituem:! Objetos gordos! Serviços externos! Bibliotecas (não quero testar se o gem funciona)!
RSpec # Expectativas" band.should_receive(:rock)" band.should_not_receive(:pause)" # Quantidade de chamadas" guitar.should_receive(:tune).once" guitar.should_receive(:play).at_least(3).times" # Argumentos" guitar.should_receive(:strum_chord).with("e")" guitar.should_receive(:strum_chord).with(:anything)" # Valores de Retorno" band.should_receive(:rocking?).and_return(true)" band.should_receive(:stop).and_raise(cantstoprocking)
# Expectativas" band.expects(:rock)" band.expects(:pause).never" Mocha # Quantidade de chamadas" guitar.expects(:tune).once" guitar.expects(:play).at_least(3) " # Argumentos" guitar.expects(:strum_chord).with("e")" guitar.expects(:strum_chord).with(:anything)" # Valores de Retorno" band.expects(:rocking?).returns(true)" band.expects(:stop).raises(cantstoprocking)
Stubs! Dublês que não verificam expectativas! Não se importam com o que aconteceu # RSpec" band.stub!(:autograph).and_return('signature')" Guitar.stub!(:find).and_return(guitar)" # Mocha" band.stubs(:autograph).returns('signature')" Guitar.stubs(:find).returns(guitar)
Meus erros com mocks/stubs! Precisar gravar muitas expectativas! Muito acoplado com implementação! Muitos stubs poucos mocks! Mockar o próprio objeto! Testes podem passar com o sistema quebrado
! Mock Roles not Objects [OOPSLA04]! Use mocks para desacoplar testes! Cuidado com mock de bibliotecas externas! Use um Adapter! Mocks não tiram o valor dos testes acoplados! CRC com testes Lições sobre mocks
class DataBranderTest < Test::Unit::TestCase" def test_saves_branded_to_storage" storage = Storage.new 'whatever'" storage.expects(:save).with('metal - rock')" DataBrander.new(storage).save_branded('rock')" end " end! class DataBrander" BRAND = "METAL"" def initialize(storage)" @storage = storage" end" def save_branded(data)" @storage.save "#{BRAND} - #{data}"" end" end!
class DataBranderTest < Test::Unit::TestCase" def test_saves_branded_to_storage" storage = Storage.new 'whatever'" storage.expects(:save).with('metal - rock')" DataBrander.new(storage).save_branded('rock')" end " end! class DataBrander" BRAND = "METAL"" def initialize(storage)" @storage = storage" end" def save_branded(data)" @storage.save "#{BRAND} - #{data}"" end" end!
class StorageTest < Test::Unit::TestCase" def test_saves_to_file" Storage.new('test.txt').save('rock')" assert_equal 'rock', File.read('test.txt')" ensure" FileUtils.rm_f('test.txt')" end" end class Storage" def initialize(filename)" @filename = filename" end" def save(val)" File.open(@filename, 'w') { f f << val}" end" end!
class StorageTest < Test::Unit::TestCase" def test_saves_to_file" Storage.new('test.txt').save('rock')" assert_equal 'rock', File.read('test.txt')" ensure" FileUtils.rm_f('test.txt')" end" end class Storage" def initialize(filename)" @filename = filename" end" def save(val)" File.open(@filename, 'w') { f f << val}" end" end!
class StorageTest < Test::Unit::TestCase" def test_saves_to_file" Storage.new('test.txt').save('rock', 'w')" assert_equal 'rock', File.read('test.txt')" ensure" FileUtils.rm_f('test.txt')" end" end class Storage" def initialize(filename)" @filename = filename" end" def save(val, mode)" File.open(@filename, mode) { f f << val}" end" end!
class StorageTest < Test::Unit::TestCase" def test_saves_to_file" Storage.new('test.txt').save('rock', 'w')" assert_equal 'rock', File.read('test.txt')" ensure" FileUtils.rm_f('test.txt')" end" end class Storage" def initialize(filename)" @filename = filename" end" def save(val, mode)" File.open(@filename, mode) { f f << val}" end" end!
storage = Storage.new("hello.txt")" data_brander = DataBrander.new(storage)" data_brander.save_branded('hello')!
storage = Storage.new("hello.txt")" data_brander = DataBrander.new(storage)" data_brander.save_branded('hello')!
Synthesis
class DataBranderTest < Test::Unit::TestCase" def test_saves_branded_to_storage" storage = Storage.new 'whatever'" storage.expects(:save).with('metal rock', 'w')" DataBrander.new(storage).save_branded('rock')" end " end! class DataBrander" BRAND = "METAL"" def initialize(storage)" @storage = storage" end" def save_branded(data)" @storage.save "#{BRAND} - #{data}", 'w'" end" end!
Synthesis
Synthesis! Cobertura de mocks! Testes acoplados somente nas bordas! Feedback rápido sobre potenciais erros! Funciona com Rspec/Mocha/Expectations! Tarefa Rake: Synthesis::Task.new('synthesis:spec') do t " t.adapter = :rspec" t.pattern = 'test_project/rspec/*_spec.rb'" end!
Visualizações
Builds rápidos!
Behaviour-Driven Development! Um processo ponta-a-ponta de desenvolvimento! Independente de ferramenta! Outside-in
Normalmente
BDD
BDD
Momento para Refletir
Resumo! Testes Unitários! Cobertura de Código! TDD! Histórias e STDD! Testes de Aceitação! Selenium! Mocks/Stubs! Synthesis! BDD
Por que testar?
Custo de encontrar um erro
Feedback! Confiança! Erros são detectados rapidamente! Ajudam a saber quando terminamos! Pensar no design antes de implementar! Evita generalização desnecessária! Regressão automatizada
Build Quality In Work smarter, not harder -- Tom de Marco, Slack Inspecionar para previnir defeitos é bom; Inspecionar para encontrar defeitos é desperdício -- Shigeo Shingo, The Toyota Production System
Auto-Inspeção
Bugs Bugs são testes que você esqueceu de escrever! Resolva a Causa Raiz
Verificação e Validação
Obrigado! Perguntas? dsato@thoughtworks.com www.dtsato.com