Dublês
Mock, spy e stub — e por que aqui são um objeto só.
Um dublê finge ser outro objeto e anota o que lhe pediram. Serve para três coisas que costumam se confundir:
| O que faz | |
|---|---|
| stub | devolve o que você mandou devolver |
| spy | deixa passar para o real e anota as chamadas |
| mock | as duas, mais expectativas sobre as chamadas |
Aqui é um objeto só, porque a diferença entre eles é como se usa, não o que são — e obrigar a escolher o nome certo antes de escrever o teste atrapalha mais que ajuda.
Programando respostas#
dataforge
crucible "Dublês":
trial "devolve o programado":
m := Crucible.mock("servico")
m.quando("buscar").devolve([1, 2, 3])
expect m.buscar() is [1, 2, 3]
trial "em sequência":
m := Crucible.mock()
m.quando("proximo").devolve_em_sequencia([1, 2, 3])
expect [m.proximo(), m.proximo(), m.proximo()] is [1, 2, 3]
trial "levanta o programado":
m := Crucible.mock()
m.quando("falhar").levanta("indisponível")
expect(lambda => m.falhar()).to_raise()Verificando as chamadas#
dataforge
crucible "Chamadas":
trial "anota tudo":
m := Crucible.mock("api")
m.buscar(1)
m.buscar(2)
m.salvar("x")
expect m.vezes("buscar") is 2
expect m.foi_chamado("salvar") is yes
expect m.foi_chamado("apagar") is no
expect m.chamado_com("salvar", "x") is yes
expect m.argumentos_de("buscar") is [[1], [2]]Spy: deixa passar e anota#
dataforge
blueprint Calculadora:
action dobro(x):
yield x * 2
crucible "Spy":
trial "o real roda, e a chamada fica registrada":
espiao := Crucible.spy(spawn Calculadora())
expect espiao.dobro(4) is 8
expect espiao.chamado_com("dobro", 4) is yesStub: só as respostas#
dataforge
crucible "Stub":
trial "programado de uma vez":
s := Crucible.stub({"buscar": [1, 2], "contar": 2})
expect s.buscar() is [1, 2]
expect s.contar() is 2Onde isso encaixa no projeto#
Dublê é o que torna inversão de dependência prática. Um serviço que recebe o repositório por parâmetro se testa sem banco nenhum:
dataforge
trait Repositorio:
action salvar(nome)
blueprint Servico:
action setup(repositorio):
self.repositorio := repositorio
action cadastrar(nome):
given len(nome) smaller 2:
trigger "nome curto demais"
yield self.repositorio.salvar(nome)
crucible "Cadastro":
trial "salva o que passa na validação":
repo := Crucible.mock("repositorio")
repo.quando("salvar").devolve("Ana")
s := spawn Servico(repo)
expect s.cadastrar("Ana") is "Ana"
expect repo.chamado_com("salvar", "Ana") is yes
trial "não salva o que não passa":
repo := Crucible.mock("repositorio")
s := spawn Servico(repo)
expect(lambda => s.cadastrar("A")).to_raise()
expect repo.foi_chamado("salvar") is noCongelando o relógio#
Todo teste que toca data e hora falha uma vez por ano, na virada, se não congelar:
dataforge
crucible "Tempo":
trial "instante fixo":
relogio := Crucible.freeze_time(1700000000)
expect yes
relogio.liberar()