RSpec: Behaviour-Driven Development On Ruby

Posted on April 06, 2007

Escrevi um pouco apressado, estarei viajando daqui a pouco e como já tinha uma parte no ‘draft’, resolvi publicar logo. :)

O código apresentado nos exemplos está utilizando os métodos da versão do RSpec 0.9, que foi lançado ontem na lista de usuários e o site do projeto ainda apresenta a versão 0.8.2(até o momento), porém nada complicado de “converter”.

Então, aqui vamos. :)

Pequena Introdução

Muito, e há muito tempo, tem se falado de Desenvolvimento orientado a Testes, ou TDD(Test-Driven Development). Metodologias, como a Extreme Programming, que fazem sua utilização, além de frameworks e ferramentas para as mais diversas linguagens. “Mas será que estamos fazendo corretamente? TDD é realmente sobre testes?”. Foram alguns dos questionamentos que levaram a Dave Astels e Dan North a criarem outra alternativa: BDD, Behaviour Driven Development, ou Desenvolvimento orientado a Comportamento. Notaram que experientes praticantes de TDD chegavam a um ponto que percebiam que não estavam escrevendo testes e sim definindo comportamentos. Tendo isso em mente, desenvolveram frameworks que permitissem um melhor foco nessas definições: JBehave e RSpec(tema deste post).


Instalando


gem install rspec

Possui algumas dependências como o rcov e o rake, acesse a documentação para outras.

Estrutura básica


#no "describe" descrevemos o que estamos especificando no momento
describe Carteira do 

    #este método é chamado antes de cada "it", pode ser utilizado para indicar um estado dos objetos envolvidos e mocks.
    setup do
        ...
    end

    #nos métodos "it" é onde realmente trabalhamos
    it "deve ter um método para entrada $" do
        ...
    end

    it "não deve estar vazia" {
        ...
    }

end

#podemos definir mais de um describe por arquivo.
describe Carteira, " (validações)" do #este describe resultaria em: "Carteira (validações)" 
    ...
end

Should e Expectations


O RSpec adiciona os métodos should(‘devia’) e should_not(‘não devia’) à Object, tornando disponível para qualquer objeto de sua aplicação (durantes os testes :) ):
#exemplos
carteira.should_not ...
post.should_not ...
post.save!.should ...

E também define um conjunto de Expectations(‘expectativas’) para que possamos definir os comportamentos esperados.
#exemplos
be_...(...)
have(...).(...)
raise_error(...)
‘Juntando’:
carteira.should_not be_a_kind_of(Carro)
#carteira não devia ser do tipo Carro

post.should_not have(3).comentarios
#post não deve ter 3 comentários

post.save!.should raise_error
#post.save! devia causar erro

A leitura flui melhor do que ‘assert aquilo’, não? :) Veja outros Expectations.

Exemplo


Como exemplo vamos definir o comportamento de Carteira.

Primeiro criamos o “esqueleto”:
#arquivo carteira_spec.rb
require 'carteira' #vamos criar mais abaixo
describe Carteira do

    setup do
        @carteira = Carteira.new
    end
end

Agora vamos para o primeiro comportamento:

  • o valor inicial de dinheiro deve ser 0. :|

it "o valor inicial de dinheiro deve ser 0 :|" do
    @carteira.dinheiro.should == 0
    @carteira.dinheiro.should eql?(0) #alternativa 1
    @carteira.dinheiro.should be_zero #alternativa 2        
end

Três formas de (d)escrever. Na última o “be_zero” na verdade está utilizando o método “zero?” de dinheiro(Fixnum), servindo para qualquer ‘predicate’ (método terminado em ’?’). Ex: be_include (Array:include?), be_empty(String:empty?).

Mais dois comportamentos:
  • o método ganho(x) deve incrementar o dinheiro em x. :)
  • o método gasto(x) deve decrementar o dinheiro em x, lançar erro SemGranaError caso o valor for se tornar negativo. :(

it "o método ganho(x) deve incrementar o dinheiro em x. :)" do
    @carteira.ganho(3)
    @carteira.dinheiro.should == 3 #outra forma! =]        
end

it "o método gasto(x) deve decrementar o dinheiro em x, lançar erro SemGranaError caso o valor for se tornar negativo. :(" do
    lambda{ @carteira.gasto(3) }.should raise_error(SemGranaError)

    @carteira.ganho(3)
    @carteira.gasto(3).should == 0        
end

Fácil, não? Agora vamos criar nossa Carteira, para podermos executar os testes(?):

#arquivo carteira.rb
class Carteira

    attr_accessor :dinheiro

    def initialize
    end

    def ganho(dinheiro)
    end

    def gasto(dinheiro)
    end

end
Para executar: spec carteira_spec.rb. Como saída teremos:
FFF

1)
NoMethodError in 'Carteira o valor inicial de dinheiro deve ser 0 :|'
undefined method `dinheiro' for #<Carteira:0xb7c51270>
./carteira_spec.rb:10:

2)
NoMethodError in 'Carteira o método ganho(x) deve incrementar o dinheiro em x. :)'
undefined method `dinheiro' for #<Carteira:0xb7c3b484>
./carteira_spec.rb:17:

3)
NameError in 'Carteira o método gasto(x) deve decrementar o dinheiro em x, lançar erro SemGranaError caso o valor for se tornar negativo. :('
uninitialized constant SemGranaError
./carteira_spec.rb:21:

Finished in 0.009035 seconds

3 examples, 3 failures

Cada ‘F’ no início representa uma falha e cada tópico descreve uma falha. É interessante que tudo falhe na primeira vez, para sabermos que funcionou depois que implementamos (oras!).

Implementando a primeira especificação:
    def initialize
        @dinheiro = 0
    end
Executando(spec carteira_spec.rb), temos:
.FF

1)
'Carteira o método ganho(x) deve incrementar o dinheiro em x. :)' FAILED
expected 3, got 0 (using ==)
./carteira_spec.rb:17:

2)
NameError in 'Carteira o método gasto(x) deve decrementar o dinheiro em x, lançar erro SemGranaError caso o valor for se tornar negativo. :('
uninitialized constant SemGranaError
./carteira_spec.rb:21:

Finished in 0.008884 seconds

3 examples, 2 failures

“E cadê o que eu fiz?”. Virou um ’.’, aquele pontinho quer dizer que funcionou.

Finalizando implementando o resto:
#arquivo carteira.rb
class Carteira

    attr_accessor :dinheiro

    def initialize
        @dinheiro = 0
    end

    def ganho(dinheiro)
        @dinheiro = @dinheiro + dinheiro
    end

    def gasto(dinheiro)
        raise SemGranaError if (@dinheiro - dinheiro) < 0
        @dinheiro = @dinheiro - dinheiro
    end

end

class SemGranaError < StandardError

end
Saída:
...

Finished in 0.007725 seconds

3 examples, 0 failures

Experimente outras opções de formatação, para olhar as opções execute: spec – -help

Fim!

Gostou? Se sim, no próximo projeto utilize o RSpec, ou alguma ferramenta BDD ou TDD, seu aplicativo ficará saudável, não tem mais desculpa. (Não preparei nada para a alternativa ‘não’ :D ).

Já estou preparando posts sobre mock e rspec_on_rails.

Comporte-se e até a próxima.

Comments
  1. Luciano PachecoApril 06, 2007 @ 05:44 PM
    Bem legal esse BDD mesmo! Fica muito mais natural e intuitivo. Vou dar uma googleada sobre isso. :) Até mais.
  2. Carlos EduardoJuly 24, 2007 @ 07:24 PM
    Hummm interessante isto, gostaria de ver mais posts sobre este tema cara, isso me abriu as idéias para criar uma ferramenta visual para isto =D Vai vai, me segura agora! hehehhee
  3. Roberto SoaresJuly 25, 2007 @ 03:35 PM
    Opa! Carlos, vou te passar umas dicas que podem te ajudar a fazer algo. De cara, dá uma olhada de como implementar um relatório customizado. Abraços! =]
Post a comment
Comment