Introdução ao RSpec e ao Desenvolvimento Guiado por Testes: Um Guia Visual e Prático
Aprenda RSpec e desenvolvimento guiado por testes construindo um método de fatorial, um teste que falha de cada vez: vermelho, verde, refatorar, como ler falhas, matchers, casos extremos, resultados reais rodada por rodada, medições de velocidade e exercícios com respostas.

Nesta página
Como você sabe que o seu código funciona? Você roda, testa algumas entradas e confere as respostas no olho. Isso funciona uma vez. Mas cada vez que você mudar o código teria que testar tudo de novo, e não vai. Testes automatizados fazem essas tentativas por você, em uma fração de segundo, todas as vezes.
O RSpec é a ferramenta de testes mais popular do Ruby, e o desenvolvimento guiado por testes (TDD, do inglês test-driven development) é um jeito de trabalhar em que você escreve o teste antes do código. Neste guia você vai construir um método fatorial do jeito TDD, um teste que falha de cada vez, e ver cada mensagem real que o RSpec imprime pelo caminho. Você vai aprender a ler falhas, usar matchers, cuidar de casos extremos e manter o código seguro enquanto o arruma. Exercícios com respostas fecham o artigo.
Todas as saídas abaixo vêm de execuções reais com o RSpec 3.13 no Ruby 3.3. Os seus tempos ("Finished in…") vão variar um pouco de uma execução para outra. O RSpec imprime as mensagens em inglês; vamos traduzindo as partes importantes.
O ciclo do desenvolvimento guiado por testes

O TDD tem três passos, com nomes das cores que as ferramentas de teste imprimem:
- Vermelho: escreva um teste pequeno para algo que o código deve fazer. Rode e veja ele falhar (afinal, ainda não existe código).
- Verde: escreva o código mais simples que faz esse teste passar. Rode todos os testes.
- Refatorar: arrume o código mantendo todos os testes verdes.
Depois repita com o próximo pedacinho de comportamento. Os testes vão se acumulando numa rede de segurança que confere tudo o que você construiu, toda vez que você os roda.
O nosso objetivo: fatorial

O fatorial de um número, escrito n!, multiplica o número por todos os números inteiros abaixo dele até chegar no 1: 4! = 4 × 3 × 2 × 1 = 24. Os casos base são 1! = 1 e, por definição, 0! = 1.
Repare que a tabela já é uma lista de testes: cada linha é uma entrada com a resposta que esperamos.
Preparando tudo
Instale o RSpec (uma vez por computador) e confira a versão:
1$ gem install rspec
2$ rspec --version
3RSpec 3.13
4 - rspec-core 3.13.6
5 - rspec-expectations 3.13.5
6 - rspec-mocks 3.13.8
7 - rspec-support 3.13.7Crie uma pasta para o projeto, entre nela e deixe o RSpec se configurar:
1$ mkdir fatorial
2$ cd fatorial
3$ rspec --init
4 create .rspec
5 create spec/spec_helper.rb
Depois crie um fatorial.rb vazio para o código e um spec/fatorial_spec.rb para os testes. O RSpec roda todo arquivo dentro de spec/ cujo nome termina em _spec.rb, então essas duas regras de nome importam.
Vermelho: o primeiro teste
Coloque isto no spec/fatorial_spec.rb:
1require_relative "../fatorial"
2
3RSpec.describe "fatorial" do
4 it "devolve 1 para 0" do
5 expect(fatorial(0)).to eq(1)
6 end
7end
describe("descreva") dá nome à coisa sendo testada e agrupa os exemplos dela.it("ele") é um exemplo: um único comportamento, descrito em palavras.expect(real).to matcher("espere que real seja…") é a verificação. Aqui o matchereq(1)quer dizer "igual a 1".
Em inglês, isso se lê quase como uma frase: expect fatorial of 0 to equal 1, "espere que o fatorial de 0 seja igual a 1". Esse jeito legível é o estilo do RSpec, mas não há mágica nenhuma: describe, it, expect e eq são métodos comuns do Ruby que recebem strings e blocos.
Rode os testes a partir da pasta do projeto (a que tem a spec/):
1$ rspec
2F
3
4Failures:
5
6 1) fatorial devolve 1 para 0
7 Failure/Error: expect(fatorial(0)).to eq(1)
8
9 NoMethodError:
10 undefined method `fatorial' for #<RSpec::ExampleGroups::Fatorial:0x0000028ef1564f00>
11 # ./spec/fatorial_spec.rb:5:in `block (2 levels) in <top (required)>'
12
13Finished in 0.03429 seconds (files took 0.46926 seconds to load)
141 example, 1 failure
15
16Failed examples:
17
18rspec ./spec/fatorial_spec.rb:4 # fatorial devolve 1 para 0Vermelho, como esperado: ainda não existe o método fatorial ("undefined method", método não definido). (O número 0x0000… é um endereço de memória e muda a cada execução.) Cada F na primeira linha é um exemplo que falhou; um que passa imprime um ponto.
Verde, um passinho de cada vez
No TDD você deixa a falha dizer o que fazer em seguida, e faz só isso. Defina o método:
def fatorial
end1$ rspec
2F
3
4Failures:
5
6 1) fatorial devolve 1 para 0
7 Failure/Error: expect(fatorial(0)).to eq(1)
8
9 ArgumentError:
10 wrong number of arguments (given 1, expected 0)
11 # ./fatorial.rb:1:in `fatorial'
12 # ./spec/fatorial_spec.rb:5:in `block (2 levels) in <top (required)>'
13
14Finished in 0.02019 seconds (files took 0.52359 seconds to load)
151 example, 1 failure
16
17Failed examples:
18
19rspec ./spec/fatorial_spec.rb:4 # fatorial devolve 1 para 0Uma falha nova é progresso. "Wrong number of arguments (given 1, expected 0)": o teste passa um argumento, mas o método não recebe nenhum. Acrescente um parâmetro:
def fatorial(numero)
end1$ rspec
2F
3
4Failures:
5
6 1) fatorial devolve 1 para 0
7 Failure/Error: expect(fatorial(0)).to eq(1)
8
9 expected: 1
10 got: nil
11
12 (compared using ==)
13 # ./spec/fatorial_spec.rb:5:in `block (2 levels) in <top (required)>'
14
15Finished in 0.0271 seconds (files took 0.41189 seconds to load)
161 example, 1 failure
17
18Failed examples:
19
20rspec ./spec/fatorial_spec.rb:4 # fatorial devolve 1 para 0Agora o método roda, mas um método vazio devolve nil: esperado (expected) 1, obtido (got) nil. Qual é o código mais simples que devolve 1? Devolver 1:
def fatorial(numero)
1
end1$ rspec
2.
3
4Finished in 0.02445 seconds (files took 0.41461 seconds to load)
51 example, 0 failuresVerde! Devolver uma constante parece trapaça, e é, de propósito: o código só fica tão esperto quanto os testes exigem. Os próximos testes vão obrigá-lo a ficar mais esperto.
O caminho até aqui:
| Rodada | O RSpec disse | O que quer dizer | O conserto |
|---|---|---|---|
| 1 | NoMethodError: undefined method 'fatorial' | O método não existe | Defina o método |
| 2 | ArgumentError: wrong number of arguments (given 1, expected 0) | Ele recebe o número errado de argumentos | Acrescente um parâmetro |
| 3 | expected: 1, got: nil | Ele roda, mas devolve o valor errado | Devolva 1 |
| 4 | 1 example, 0 failures | Verde | Escreva o próximo teste |
Como ler uma falha

Uma saída mais legível: o formato de documentação
O rspec --init criou um arquivo .rspec com opções que o RSpec usa em toda execução. Acrescente uma segunda linha para ele imprimir cada exemplo pelo nome:
--require spec_helper
--format documentationMais testes, código mais esperto
Acrescente mais dois exemplos ao spec:
1require_relative "../fatorial"
2
3RSpec.describe "fatorial" do
4 it "devolve 1 para 0" do
5 expect(fatorial(0)).to eq(1)
6 end
7
8 it "devolve 1 para 1" do
9 expect(fatorial(1)).to eq(1)
10 end
11
12 it "devolve 2 para 2" do
13 expect(fatorial(2)).to eq(2)
14 end
15end1$ rspec
2
3fatorial
4 devolve 1 para 0
5 devolve 1 para 1
6 devolve 2 para 2 (FAILED - 1)
7
8Failures:
9
10 1) fatorial devolve 2 para 2
11 Failure/Error: expect(fatorial(2)).to eq(2)
12
13 expected: 2
14 got: 1
15
16 (compared using ==)
17 # ./spec/fatorial_spec.rb:13:in `block (2 levels) in <top (required)>'
18
19Finished in 0.01296 seconds (files took 0.3756 seconds to load)
203 examples, 1 failure
21
22Failed examples:
23
24rspec ./spec/fatorial_spec.rb:12 # fatorial devolve 2 para 2O teste novo para 1 passou de cara (a nossa constante por acaso está certa), e o para 2 falha. O conserto mais simples? Ainda um pouco atrevido:
1def fatorial(numero)
2 return 1 if numero <= 1
3
4 numero
5end1$ rspec
2
3fatorial
4 devolve 1 para 0
5 devolve 1 para 1
6 devolve 2 para 2
7
8Finished in 0.00364 seconds (files took 0.43131 seconds to load)
93 examples, 0 failuresEsse truque se chama fingir até conseguir (fake it till you make it). Para obrigar uma solução de verdade, acrescente exemplos que nenhum atalho consegue satisfazer. Isso se chama triangulação:
1 it "devolve 6 para 3" do
2 expect(fatorial(3)).to eq(6)
3 end
4
5 it "devolve 24 para 4" do
6 expect(fatorial(4)).to eq(24)
7 end
8
9 it "devolve 5040 para 7" do
10 expect(fatorial(7)).to eq(5040)
11 end1$ rspec
2
3fatorial
4 devolve 1 para 0
5 devolve 1 para 1
6 devolve 2 para 2
7 devolve 6 para 3 (FAILED - 1)
8 devolve 24 para 4 (FAILED - 2)
9 devolve 5040 para 7 (FAILED - 3)
10
11Failures:
12
13 1) fatorial devolve 6 para 3
14 Failure/Error: expect(fatorial(3)).to eq(6)
15
16 expected: 6
17 got: 3
18
19 (compared using ==)
20 # ./spec/fatorial_spec.rb:17:in `block (2 levels) in <top (required)>'
21
22 2) fatorial devolve 24 para 4
23 Failure/Error: expect(fatorial(4)).to eq(24)
24
25 expected: 24
26 got: 4
27
28 (compared using ==)
29 # ./spec/fatorial_spec.rb:21:in `block (2 levels) in <top (required)>'
30
31 3) fatorial devolve 5040 para 7
32 Failure/Error: expect(fatorial(7)).to eq(5040)
33
34 expected: 5040
35 got: 7
36
37 (compared using ==)
38 # ./spec/fatorial_spec.rb:25:in `block (2 levels) in <top (required)>'
39
40Finished in 0.02272 seconds (files took 0.42674 seconds to load)
416 examples, 3 failures
42
43Failed examples:
44
45rspec ./spec/fatorial_spec.rb:16 # fatorial devolve 6 para 3
46rspec ./spec/fatorial_spec.rb:20 # fatorial devolve 24 para 4
47rspec ./spec/fatorial_spec.rb:24 # fatorial devolve 5040 para 7Olhe os valores "got": 3, 4, 7. O método só devolve a própria entrada. Hora de escrevê-lo de verdade, multiplicando do número até o 2:
1def fatorial(numero)
2 produto = 1
3 multiplicador = numero
4
5 while multiplicador > 1
6 produto *= multiplicador
7 multiplicador -= 1
8 end
9
10 produto
11end1$ rspec
2
3fatorial
4 devolve 1 para 0
5 devolve 1 para 1
6 devolve 2 para 2
7 devolve 6 para 3
8 devolve 24 para 4
9 devolve 5040 para 7
10
11Finished in 0.00402 seconds (files took 0.52151 seconds to load)
126 examples, 0 failuresTudo verde. Os casos base também estão cobertos: para 0 e 1 o laço nunca roda e produto continua 1.
Refatorar: arrumar com uma rede de segurança
O laço while funciona, mas o Ruby consegue dizer "multiplique de 1 até n" numa linha só, com o reduce. Como os testes existem, dá para tentar sem medo:
def fatorial(numero)
(1..numero).reduce(1, :*)
end1$ rspec
2
3fatorial
4 devolve 1 para 0
5 devolve 1 para 1
6 devolve 2 para 2
7 devolve 6 para 3
8 devolve 24 para 4
9 devolve 5040 para 7
10
11Finished in 0.0038 seconds (files took 0.40621 seconds to load)
126 examples, 0 failuresContinua verde, então a versão mais curta se comporta exatamente igual em tudo o que especificamos. O reduce(1, :*) começa em 1 e vai multiplicando cada número do intervalo; para 0, o intervalo 1..0 é vazio, então a resposta é o 1 inicial.
Casos extremos: números enormes e entradas negativas
O que mais o fatorial deveria fazer? Duas boas perguntas para virar testes:
- Números grandes. Fatoriais crescem absurdamente rápido.
- Entradas inválidas. O fatorial não é definido para números negativos.

1 it "aguenta números grandes demais para 64 bits" do
2 expect(fatorial(25)).to eq(15_511_210_043_330_985_984_000_000)
3 end
4
5 it "rejeita números negativos" do
6 expect { fatorial(-1) }.to raise_error(ArgumentError)
7 endRepare nas chaves no segundo teste: expect { … } entrega ao RSpec um bloco de código para rodar, para que ele possa ficar de olho num erro. Com parênteses, o erro aconteceria antes que o RSpec pudesse pegá-lo.
1$ rspec
2
3fatorial
4 devolve 1 para 0
5 devolve 1 para 1
6 devolve 2 para 2
7 devolve 6 para 3
8 devolve 24 para 4
9 devolve 5040 para 7
10 aguenta números grandes demais para 64 bits
11 rejeita números negativos (FAILED - 1)
12
13Failures:
14
15 1) fatorial rejeita números negativos
16 Failure/Error: expect { fatorial(-1) }.to raise_error(ArgumentError)
17 expected ArgumentError but nothing was raised
18 # ./spec/fatorial_spec.rb:33:in `block (2 levels) in <top (required)>'
19
20Finished in 0.02116 seconds (files took 0.42656 seconds to load)
218 examples, 1 failure
22
23Failed examples:
24
25rspec ./spec/fatorial_spec.rb:32 # fatorial rejeita números negativosO teste de número grande passou de cara: os inteiros do Ruby crescem o quanto for preciso. O teste negativo está vermelho ("expected ArgumentError but nothing was raised", esperava um ArgumentError mas nenhum erro aconteceu), porque fatorial(-1) devolve 1 em silêncio, uma resposta errada com cara de certa. Gere um erro em vez disso:
1def fatorial(numero)
2 raise ArgumentError, "o fatorial não está definido para números negativos" if numero.negative?
3
4 (1..numero).reduce(1, :*)
5end1$ rspec
2
3fatorial
4 devolve 1 para 0
5 devolve 1 para 1
6 devolve 2 para 2
7 devolve 6 para 3
8 devolve 24 para 4
9 devolve 5040 para 7
10 aguenta números grandes demais para 64 bits
11 rejeita números negativos
12
13Finished in 0.00875 seconds (files took 0.41366 seconds to load)
148 examples, 0 failuresRefatorando os testes: uma tabela de casos
Testes também são código e merecem arrumação. Aqueles seis exemplos quase idênticos podem sair de um hash de entradas e respostas, e o context ("contexto") agrupa os exemplos de uma situação:
1require_relative "../fatorial"
2
3RSpec.describe "fatorial" do
4 {
5 0 => 1,
6 1 => 1,
7 2 => 2,
8 3 => 6,
9 4 => 24,
10 7 => 5040,
11 25 => 15_511_210_043_330_985_984_000_000,
12 }.each do |numero, esperado|
13 it "devolve #{esperado} para #{numero}" do
14 expect(fatorial(numero)).to eq(esperado)
15 end
16 end
17
18 context "com um número negativo" do
19 it "gera um ArgumentError" do
20 expect { fatorial(-1) }.to raise_error(ArgumentError, /negativos/)
21 end
22 end
23end1$ rspec
2
3fatorial
4 devolve 1 para 0
5 devolve 1 para 1
6 devolve 2 para 2
7 devolve 6 para 3
8 devolve 24 para 4
9 devolve 5040 para 7
10 devolve 15511210043330985984000000 para 25
11 com um número negativo
12 gera um ArgumentError
13
14Finished in 0.00821 seconds (files took 0.41454 seconds to load)
158 examples, 0 failuresAs mesmas oito verificações, menos repetição, e acrescentar um caso agora é uma linha só. O /negativos/ também confere a mensagem do erro. (O guia de hashes explica o each com dois parâmetros no bloco.)
A sessão inteira de uma vez

Cada barra vermelha é um teste que falhou antes de existir o código para fazê-lo passar. É a prova de que cada teste consegue mesmo pegar um problema.
Os testes pegam uma refatoração quebrada
Eis por que a rede de segurança importa. Imagine que alguém "simplifica" o método, achando que multiplicar por 1 não serve para nada:
1def fatorial(numero)
2 raise ArgumentError, "o fatorial não está definido para números negativos" if numero.negative?
3
4 (2..numero).reduce(:*)
5endParece certo e funciona para 2, 3, 4… Mas:
1$ rspec
2
3fatorial
4 devolve 1 para 0 (FAILED - 1)
5 devolve 1 para 1 (FAILED - 2)
6 devolve 2 para 2
7 devolve 6 para 3
8 devolve 24 para 4
9 devolve 5040 para 7
10 devolve 15511210043330985984000000 para 25
11 com um número negativo
12 gera um ArgumentError
13
14Failures:
15
16 1) fatorial devolve 1 para 0
17 Failure/Error: expect(fatorial(numero)).to eq(esperado)
18
19 expected: 1
20 got: nil
21
22 (compared using ==)
23 # ./spec/fatorial_spec.rb:14:in `block (3 levels) in <top (required)>'
24
25 2) fatorial devolve 1 para 1
26 Failure/Error: expect(fatorial(numero)).to eq(esperado)
27
28 expected: 1
29 got: nil
30
31 (compared using ==)
32 # ./spec/fatorial_spec.rb:14:in `block (3 levels) in <top (required)>'
33
34Finished in 0.02395 seconds (files took 0.5391 seconds to load)
358 examples, 2 failures
36
37Failed examples:
38
39rspec './spec/fatorial_spec.rb[1:1]' # fatorial devolve 1 para 0
40rspec './spec/fatorial_spec.rb[1:2]' # fatorial devolve 1 para 1(2..0) e (2..1) são intervalos vazios, e o reduce sem valor inicial devolve nil para um intervalo vazio. Os testes dos casos base pegaram o erro em milissegundos, muito antes que um usuário pudesse. (Como os exemplos saem de um laço, o RSpec os chama pela posição, como [1:1], em vez de pela linha.)
Rodando só alguns testes
Conforme a suíte cresce, muitas vezes você vai querer rodar um exemplo ou alguns:
1$ rspec spec/fatorial_spec.rb:19
2Run options: include {:locations=>{"./spec/fatorial_spec.rb"=>[19]}}
3
4fatorial
5 com um número negativo
6 gera um ArgumentError
7
8Finished in 0.00582 seconds (files took 0.4713 seconds to load)
91 example, 0 failures1$ rspec -e "negativo"
2Run options: include {:full_description=>/negativo/}
3
4fatorial
5 com um número negativo
6 gera um ArgumentError
7
8Finished in 0.00522 seconds (files took 0.48556 seconds to load)
91 example, 0 failuresrspec caminho:linharoda o exemplo (ou grupo) daquela linha.rspec -e "palavras"roda os exemplos cuja descrição completa contém essas palavras.rspec --format progresspassa por cima do.rspece imprime só pontinhos:........
Matchers: mais jeitos de conferir
O eq é só o começo. O RSpec vem com matchers para números, coleções, strings, tipos, erros e mudanças:
1RSpec.describe "matchers do dia a dia" do
2 it "compara valores" do
3 expect(2 + 2).to eq(4)
4 expect(10).to be > 3
5 expect(7).to be_between(1, 10)
6 expect(0.1 + 0.2).to be_within(0.001).of(0.3)
7 end
8
9 it "confere true, false e nil" do
10 expect([].empty?).to be true
11 expect([]).to be_empty
12 expect(nil).to be_nil
13 expect(5).not_to be_nil
14 end
15
16 it "olha dentro de strings e coleções" do
17 expect("Olá, Ruby").to include("Ruby")
18 expect("Olá, Ruby").to start_with("Olá")
19 expect("2024-07-07").to match(/\A\d{4}-\d{2}-\d{2}\z/)
20 expect([3, 1, 2]).to contain_exactly(1, 2, 3)
21 expect([2, 4, 6]).to all(be_even)
22 expect({ nome: "Ada", nascimento: 1815 }).to include(nome: "Ada")
23 end
24
25 it "confere tipos" do
26 expect(42).to be_an(Integer)
27 expect("42").to be_a(String)
28 end
29
30 it "confere erros e mudanças" do
31 expect { Integer("quarenta e dois") }.to raise_error(ArgumentError)
32 carrinho = []
33 expect { carrinho << "maçã" }.to change { carrinho.size }.from(0).to(1)
34 end
35end1$ rspec
2
3matchers do dia a dia
4 compara valores
5 confere true, false e nil
6 olha dentro de strings e coleções
7 confere tipos
8 confere erros e mudanças
9
10Finished in 0.03668 seconds (files took 0.49869 seconds to load)
115 examples, 0 failuresAlguns que valem destacar:
be_within(…).of(…)compara decimais com uma tolerância, porque0.1 + 0.2é0.30000000000000004, e não exatamente0.3.be_empty,be_evensão matchers de predicado: obe_xyzchama o métodoxyz?e espera true.not_toinverte qualquer matcher.
Bons matchers também escrevem boas mensagens de falha:
1RSpec.describe "mensagens de falha" do
2 it "explica um item que falta" do
3 expect(["maçã", "banana"]).to include("cereja")
4 end
5
6 it "mostra um diff para hashes" do
7 expect({ nome: "Ada", nascimento: 1815 }).to eq({ nome: "Ada", nascimento: 1816 })
8 end
9end1$ rspec spec/falhas_spec.rb
2
3mensagens de falha
4 explica um item que falta (FAILED - 1)
5 mostra um diff para hashes (FAILED - 2)
6
7Failures:
8
9 1) mensagens de falha explica um item que falta
10 Failure/Error: expect(["maçã", "banana"]).to include("cereja")
11 expected ["maçã", "banana"] to include "cereja"
12 # ./spec/falhas_spec.rb:3:in `block (2 levels) in <top (required)>'
13
14 2) mensagens de falha mostra um diff para hashes
15 Failure/Error: expect({ nome: "Ada", nascimento: 1815 }).to eq({ nome: "Ada", nascimento: 1816 })
16
17 expected: {:nascimento=>1816, :nome=>"Ada"}
18 got: {:nascimento=>1815, :nome=>"Ada"}
19
20 (compared using ==)
21
22 Diff:
23 @@ -1,2 +1,2 @@
24 -:nascimento => 1816,
25 +:nascimento => 1815,
26 :nome => "Ada",
27 # ./spec/falhas_spec.rb:7:in `block (2 levels) in <top (required)>'
28
29Finished in 0.03448 seconds (files took 0.38084 seconds to load)
302 examples, 2 failures
31
32Failed examples:
33
34rspec ./spec/falhas_spec.rb:2 # mensagens de falha explica um item que falta
35rspec ./spec/falhas_spec.rb:6 # mensagens de falha mostra um diff para hashesO diff aponta direto para o único valor diferente, o que salva vidas com hashes grandes.

Quão rápidos são os testes unitários?
Testes rápidos são rodados; testes lentos são pulados. Geramos specs com 10 a 10.000 exemplos de fatorial e medimos o tempo:

Cerca de 56 microssegundos por exemplo. Iniciar o Ruby e carregar os arquivos custou mais (cerca de um terço de segundo por execução) do que rodar mil exemplos. Testes unitários pequenos e focados como os nossos são baratos o bastante para rodar toda vez que você salva um arquivo.
Sua vez: pratique
Tente fazer estes antes de espiar as respostas. Monte cada um como o projeto do fatorial: o código na pasta do projeto, o spec na spec/ e --format documentation no .rspec.
Exercício 1: Adivinhe a falha (aquecimento). Qual exemplo falha, e o que o RSpec vai dizer em "got"? (Um ano é bissexto se for divisível por 4, menos os anos divisíveis por 100, a não ser que também sejam divisíveis por 400.)
def ano_bissexto?(ano)
ano % 4 == 0
end1require_relative "../ano_bissexto"
2
3RSpec.describe "ano_bissexto?" do
4 it "é true para 2024" do
5 expect(ano_bissexto?(2024)).to be true
6 end
7
8 it "é false para 2023" do
9 expect(ano_bissexto?(2023)).to be false
10 end
11
12 it "é false para 1900" do
13 expect(ano_bissexto?(1900)).to be false
14 end
15
16 it "é true para 2000" do
17 expect(ano_bissexto?(2000)).to be true
18 end
19endExercício 2: Leia a falha. Este teste falha com expected: 120, got: 0. Qual é o erro?
1def fatorial(numero)
2 produto = 0
3 (1..numero).each { |multiplicador| produto *= multiplicador }
4 produto
5endExercício 3: FizzBuzz com TDD. Escreva os testes primeiro e depois o código: fizzbuzz(numero) devolve "Fizz" para múltiplos de 3, "Buzz" para múltiplos de 5, "FizzBuzz" para múltiplos dos dois e, nos outros casos, o número como string.
Exercício 4: Palíndromos e casos extremos. Faça com TDD um método palindromo?(texto) que ignore letras maiúsculas, espaços, pontuação e acentos, para que "Socorram-me, subi no ônibus em Marrocos!" conte. Pense na string vazia também.
Exercício 5: Fibonacci (desafio). Faça com TDD o fibonacci(posicao), em que a sequência é 0, 1, 1, 2, 3, 5, 8… começando na posição 0. Use uma tabela de casos incluindo a posição 100, e rejeite posições negativas.
Quiz rápido
- Quais são os três passos do ciclo do TDD?
- Por que um teste novo deve falhar antes de você escrever o código para ele?
- Quando você escreve
expect { … }com chaves em vez deexpect(…)? - O que o
be_emptychama de verdade? - Como rodar só os exemplos cujo nome menciona "negativo"?
Respostas
Exercício 1: 1900 é divisível por 4, então o método errado diz true:
1$ rspec
2
3ano_bissexto?
4 é true para 2024
5 é false para 2023
6 é false para 1900 (FAILED - 1)
7 é true para 2000
8
9Failures:
10
11 1) ano_bissexto? é false para 1900
12 Failure/Error: expect(ano_bissexto?(1900)).to be false
13
14 expected false
15 got true
16 # ./spec/ano_bissexto_spec.rb:13:in `block (2 levels) in <top (required)>'
17
18Finished in 0.02043 seconds (files took 0.49782 seconds to load)
194 examples, 1 failure
20
21Failed examples:
22
23rspec ./spec/ano_bissexto_spec.rb:12 # ano_bissexto? é false para 1900A regra completa deixa tudo verde:
def ano_bissexto?(ano)
(ano % 4 == 0 && ano % 100 != 0) || ano % 400 == 0
end1$ rspec
2
3ano_bissexto?
4 é true para 2024
5 é false para 2023
6 é false para 1900
7 é true para 2000
8
9Finished in 0.00563 seconds (files took 0.64104 seconds to load)
104 examples, 0 failuresRepare que o 2000 passou mesmo com o erro: sem o teste do 1900, o erro teria passado batido. Bons testes vão atrás dos casos traiçoeiros.
Exercício 2: O produto começa em 0, e 0 vezes qualquer coisa é 0. Um produto tem que começar em 1 (assim como uma soma começa em 0). Trocar produto = 0 por produto = 1 faz o teste passar.
1$ rspec
2
3fatorial
4 devolve 120 para 5 (FAILED - 1)
5
6Failures:
7
8 1) fatorial devolve 120 para 5
9 Failure/Error: expect(fatorial(5)).to eq(120)
10
11 expected: 120
12 got: 0
13
14 (compared using ==)
15 # ./spec/fatorial_spec.rb:5:in `block (2 levels) in <top (required)>'
16
17Finished in 0.01436 seconds (files took 0.46315 seconds to load)
181 example, 1 failure
19
20Failed examples:
21
22rspec ./spec/fatorial_spec.rb:4 # fatorial devolve 120 para 5Exercício 3: Um teste por regra. No código, confira "os dois" primeiro, senão o 15 pararia no "Fizz":
1require_relative "../fizzbuzz"
2
3RSpec.describe "fizzbuzz" do
4 it "devolve o número como texto" do
5 expect(fizzbuzz(1)).to eq("1")
6 end
7
8 it "devolve Fizz para múltiplos de 3" do
9 expect(fizzbuzz(9)).to eq("Fizz")
10 end
11
12 it "devolve Buzz para múltiplos de 5" do
13 expect(fizzbuzz(10)).to eq("Buzz")
14 end
15
16 it "devolve FizzBuzz para múltiplos dos dois" do
17 expect(fizzbuzz(30)).to eq("FizzBuzz")
18 end
19end1def fizzbuzz(numero)
2 return "FizzBuzz" if numero % 15 == 0
3 return "Fizz" if numero % 3 == 0
4 return "Buzz" if numero % 5 == 0
5
6 numero.to_s
7end1$ rspec
2
3fizzbuzz
4 devolve o número como texto
5 devolve Fizz para múltiplos de 3
6 devolve Buzz para múltiplos de 5
7 devolve FizzBuzz para múltiplos dos dois
8
9Finished in 0.00392 seconds (files took 0.43658 seconds to load)
104 examples, 0 failuresExercício 4: Cada exigência virou um exemplo, mais o caso extremo da string vazia:
1require_relative "../palindromo"
2
3RSpec.describe "palindromo?" do
4 it "é true para um palíndromo simples" do
5 expect(palindromo?("osso")).to be true
6 end
7
8 it "é false para uma palavra que não é" do
9 expect(palindromo?("ruby")).to be false
10 end
11
12 it "ignora letras maiúsculas" do
13 expect(palindromo?("Arara")).to be true
14 end
15
16 it "ignora espaços, pontuação e acentos" do
17 expect(palindromo?("Socorram-me, subi no ônibus em Marrocos!")).to be true
18 end
19
20 it "é true para uma string vazia" do
21 expect(palindromo?("")).to be true
22 end
23end1def palindromo?(texto)
2 letras = texto.unicode_normalize(:nfd).downcase.gsub(/[^a-z0-9]/, "")
3 letras == letras.reverse
4end1$ rspec
2
3palindromo?
4 é true para um palíndromo simples
5 é false para uma palavra que não é
6 ignora letras maiúsculas
7 ignora espaços, pontuação e acentos
8 é true para uma string vazia
9
10Finished in 0.0489 seconds (files took 0.41899 seconds to load)
115 examples, 0 failuresO unicode_normalize(:nfd) separa cada letra acentuada em letra mais acento (o "ô" vira "o" seguido de "^"), e aí o gsub tira só o acento: "ônibus" vira "onibus". Sem isso, o gsub apagaria o "ô" inteiro e sobraria "nibus". (O guia de métodos de string explica o downcase, o gsub e o reverse.)
Exercício 5: Uma tabela de casos mais o caso do erro. A resposta de 21 dígitos da posição 100 não é problema para o Ruby:
1require_relative "../fibonacci"
2
3RSpec.describe "fibonacci" do
4 { 0 => 0, 1 => 1, 2 => 1, 3 => 2, 10 => 55, 100 => 354_224_848_179_261_915_075 }.each do |posicao, esperado|
5 it "devolve #{esperado} na posição #{posicao}" do
6 expect(fibonacci(posicao)).to eq(esperado)
7 end
8 end
9
10 it "rejeita posições negativas" do
11 expect { fibonacci(-1) }.to raise_error(ArgumentError, /negativa/)
12 end
13end1def fibonacci(posicao)
2 raise ArgumentError, "a posição não pode ser negativa" if posicao.negative?
3
4 anterior, atual = 0, 1
5 posicao.times { anterior, atual = atual, anterior + atual }
6 anterior
7end1$ rspec
2
3fibonacci
4 devolve 0 na posição 0
5 devolve 1 na posição 1
6 devolve 1 na posição 2
7 devolve 2 na posição 3
8 devolve 55 na posição 10
9 devolve 354224848179261915075 na posição 100
10 rejeita posições negativas
11
12Finished in 0.00781 seconds (files took 0.39706 seconds to load)
137 examples, 0 failuresQuiz: (1) Vermelho (escreva um teste que falha), verde (faça passar do jeito mais simples) e refatorar (arrume mantendo tudo verde). (2) Um teste que nunca falhou pode não estar conferindo nada; vê-lo falhar prova que ele consegue pegar o problema. (3) Quando você confere algo que acontece enquanto o código roda, como raise_error ou change: o RSpec precisa do código como bloco para rodá-lo ele mesmo. (4) O método empty?: o be_xyz chama xyz?. (5) rspec -e "negativo".
Cola rápida
| Eu quero… | Escreva |
|---|---|
| Instalar o RSpec | gem install rspec |
| Preparar um projeto | rspec --init (cria o .rspec e o spec/spec_helper.rb) |
| Agrupar os testes de algo | RSpec.describe "fatorial" do … end |
| Agrupar testes de uma situação | context "com um número negativo" do … end |
| Escrever um teste | it "devolve 1 para 0" do … end |
| Conferir um valor | expect(fatorial(0)).to eq(1) |
| Conferir o contrário | expect(valor).not_to be_nil |
| Conferir um erro | expect { codigo }.to raise_error(ArgumentError) |
| Rodar todos os testes | rspec (da pasta que tem a spec/) |
| Rodar um arquivo / uma linha | rspec spec/fatorial_spec.rb / rspec spec/fatorial_spec.rb:19 |
| Rodar testes pelo nome | rspec -e "negativo" |
| Mostrar o nome dos testes | --format documentation no .rspec |
Resumindo
Testar transforma "acho que funciona" em "sei que funciona, e vou saber na hora em que parar de funcionar":
- O TDD é um ciclo: vermelho (um teste que falha), verde (o código mais simples que passa), refatorar (arrumar mantendo tudo verde).
- Deixe as falhas guiarem você. Cada mensagem (sem método, argumentos errados, valor errado) diz qual é o próximo passinho.
- Finja e depois triangule. Acrescente exemplos até que só a solução de verdade consiga passar em todos.
- Specs se leem como frases:
describeo quê,itfaz algo,expect(real).to matcher. - Os testes são uma rede de segurança: deixam você refatorar com coragem e pegam erros como o
nildo intervalo vazio em milissegundos. - Pense nos casos extremos: zero, um, valores enormes, entradas negativas ou vazias.
- Testes unitários são rápidos: cerca de 56 microssegundos por exemplo na nossa medição.
Para mais Ruby, leia O Que É a Linguagem de Programação Ruby?, Arrays em Ruby, Hashes em Ruby e Métodos de String em Ruby.
Leia a seguir

O Que É a Linguagem de Programação Rust? Um Guia Simples da Linguagem Rápida Que Não Deixa Você Travar
Um guia simples e sem jargões sobre o Rust, a linguagem tão rápida quanto o C que impede famílias inteiras de erros de memória antes de o programa rodar. Entenda a ideia de posse, por que gigantes da tecnologia o adotam e onde ele é usado.

O Que É a Linguagem de Programação Go? Um Guia Simples da Linguagem da Nuvem
Um guia simples e sem jargões sobre o Go, a linguagem simples e rápida que o Google criou para a nuvem. Entenda o que são goroutines, por que um programa Go é um arquivo só e como o Go passou a mover o Docker, o Kubernetes e boa parte da internet.

O Que É a Linguagem de Programação Ruby? Um Guia Simples da Linguagem Feita para a Felicidade
Um guia simples e sem jargões sobre o Ruby, a linguagem de programação feita para deixar os programadores felizes. Entenda por que o código Ruby parece inglês, como o Ruby on Rails move sites como GitHub e Shopify, e onde o Ruby brilha.
Comentários
Seja gentil e específico. Comentários com links de contas novas aparecem depois da análise de um moderador.