Um teste para place_order precisa de um gateway de pagamento, um cliente de SMS e uma calculadora de impostos. Use os de verdade e cada execução do teste cobra um cartão e manda um SMS para um número de telefone real. Um teste unitário tem que rodar em milissegundos e sem rede, então os três precisam ser substituídos por outra coisa. Esse “outra coisa” vem em cinco tipos, cada um resolvendo um problema diferente, e tratá-los como intercambiáveis é por isso que as discussões de “só usa um mock” em code review não terminam nunca.

O que é um test double

Gerard Meszaros cunhou o termo “test double” no livro xUnit Test Patterns como o termo genérico para qualquer coisa que substitui uma dependência real durante um teste, da mesma forma que um dublê substitui um ator. Equipes de filmagem usam dublês diferentes para cenas diferentes: um só precisa ficar no fundo com o penteado certo, outro realmente faz a cena de carro batendo. Os test doubles se dividem do mesmo jeito. Dummy, stub, spy, fake e mock cumprem cinco funções diferentes, e o que os separa é o que o teste verifica, não o quão elaborado é o objeto substituto.

Esta é a classe que todos os exemplos abaixo testam. Vale a pena substituir três dessas dependências; a quarta acaba não importando:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class PaymentDeclined(Exception):
    pass


class OrderProcessor:
    def __init__(self, gateway, sms_client, tax_calculator, logger):
        self.gateway = gateway
        self.sms_client = sms_client
        self.tax_calculator = tax_calculator
        self.logger = logger

    def place_order(self, customer_phone, subtotal):
        total = self.tax_calculator.total_with_tax(subtotal)
        try:
            charge_id = self.gateway.charge(total)
        except PaymentDeclined:
            self.logger.warning("payment declined for %s", customer_phone)
            raise
        self.sms_client.send(customer_phone, f"Order confirmed, charged ${total:.2f}")
        return charge_id

logger só é chamado no caminho de erro. Isso importa no primeiro exemplo.

Dois substitutos pequenos são reaproveitados nos próximos testes, por isso são definidos uma única vez aqui:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from unittest.mock import Mock, create_autospec


class StubGateway:
    def __init__(self, charge_id):
        self.charge_id = charge_id

    def charge(self, amount):
        return self.charge_id


class StubTaxCalculator:
    def __init__(self, total):
        self.total = total

    def total_with_tax(self, subtotal):
        return self.total

O que é um dummy object em testes

Um dummy é passado só para satisfazer um parâmetro que o teste nunca exercita. Ele não faz nada e não verifica nada. O teste do caminho feliz abaixo nunca chega ao branch de exceção, então logger pode ser qualquer coisa que sobreviva a ser atribuída a um atributo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def test_place_order_returns_the_charge_id():
    dummy_logger = object()  # nunca chamado nesse caminho
    processor = OrderProcessor(
        gateway=StubGateway(charge_id="ch_1"),
        sms_client=Mock(),
        tax_calculator=StubTaxCalculator(total=108.00),
        logger=dummy_logger,
    )

    charge_id = processor.place_order("+15551234567", subtotal=100.00)

    assert charge_id == "ch_1"

object() funciona porque nada chama um método nele. Se uma mudança futura fizesse place_order registrar log também no caminho de sucesso, esse teste falharia com um AttributeError. Essa falha é útil: ela avisa que o dummy parou de ser um dummy.

O que é um stub em testes unitários

Um stub devolve uma resposta fixa para manter o resto do teste determinístico. A calculadora de impostos real poderia chamar uma API de alíquotas. O teste não quer isso, então StubTaxCalculator devolve 108.00 independente do subtotal recebido. Você já viu isso em ação no teste do dummy acima, junto com StubGateway fazendo o mesmo trabalho para charge.

Nada verifica se total_with_tax foi chamado, ou com qual argumento. Um stub alimenta o código testado com uma entrada controlada; ele nunca verifica uma interação. Essa é a linha que separa um stub dos próximos três test doubles.

O que é um test spy

Um spy registra as chamadas feitas a ele para que o teste possa inspecioná-las depois que o código testado rodou. O unittest.mock.Mock registra toda chamada automaticamente, então a maioria das suítes Python o usa como spy mesmo quando a palavra nunca aparece no teste:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def test_place_order_sends_a_confirmation_sms():
    sms_spy = Mock()
    processor = OrderProcessor(
        gateway=StubGateway(charge_id="ch_1"),
        sms_client=sms_spy,
        tax_calculator=StubTaxCalculator(total=108.00),
        logger=object(),
    )

    processor.place_order("+15551234567", subtotal=100.00)

    sms_spy.send.assert_called_once_with(
        "+15551234567", "Order confirmed, charged $108.00"
    )

Nada foi configurado em sms_spy antes da chamada. Ele aceitou o que recebeu, registrou e o teste o questionou depois.

O que é um fake object em testes

Um stub não consegue dizer se duas cobranças seguidas recebem dois IDs diferentes, porque ele só conhece um valor. Quando um teste precisa de comportamento ao longo de várias chamadas, você quer um fake: uma implementação pequena que de fato funciona.

1
2
3
4
5
6
7
8
class FakeGateway:
    def __init__(self):
        self.charges = []

    def charge(self, amount):
        charge_id = f"ch_{len(self.charges) + 1}"
        self.charges.append((charge_id, amount))
        return charge_id
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def test_two_orders_get_two_different_charge_ids():
    gateway = FakeGateway()
    processor = OrderProcessor(
        gateway=gateway,
        sms_client=Mock(),
        tax_calculator=StubTaxCalculator(total=108.00),
        logger=object(),
    )

    first_id = processor.place_order("+15551234567", subtotal=100.00)
    second_id = processor.place_order("+15557654321", subtotal=100.00)

    assert first_id != second_id
    assert gateway.charges == [(first_id, 108.00), (second_id, 108.00)]

FakeGateway não faz chamada de rede nem tem um processador de pagamento por trás, mas mantém estado da mesma forma que o real. O teste precisava exatamente desse estado, e um stub não conseguiria entregar isso.

O que é um mock object

Na definição de Meszaros, um mock carrega suas expectativas para dentro do teste: as regras de uma chamada válida ficam fixadas antes do código rodar, e uma chamada que quebra essas regras falha na hora. O unittest.mock é mais flexível que isso, já que você ainda verifica depois, mas create_autospec cobre a parte importante. Ele constrói o double a partir de uma classe real, então cada chamada é verificada contra as assinaturas reais dos métodos no momento em que acontece:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Gateway:
    def charge(self, amount: float) -> str:
        raise NotImplementedError


def test_place_order_charges_the_amount_after_tax():
    gateway = create_autospec(Gateway, instance=True)
    gateway.charge.return_value = "ch_1"
    processor = OrderProcessor(
        gateway=gateway,
        sms_client=Mock(),
        tax_calculator=StubTaxCalculator(total=108.00),
        logger=object(),
    )

    processor.place_order("+15551234567", subtotal=100.00)

    gateway.charge.assert_called_once_with(108.00)

Mude place_order para chamar self.gateway.charge(total, currency="usd") e esse teste falha com um TypeError dentro de place_order, porque Gateway.charge não aceita o argumento currency. Agora imagine um Mock() simples no mesmo lugar, com alguém atualizando a asserção para corresponder à nova chamada. O teste passa, e o código quebra na primeira vez que encontra o gateway real. Um spy confirma que uma chamada aconteceu. Um mock com autospec também confirma que a chamada é uma que o objeto real aceitaria.

Mock vs stub vs spy vs fake: tabela comparativa

Test doubleO que fazUse quando
DummyPassado para satisfazer um parâmetro; nunca chamadoUma dependência existe só porque a assinatura exige isso nesse caminho
StubDevolve uma resposta fixa, independente da entradaO teste precisa de um valor fixo e determinístico vindo de uma dependência
SpyRegistra chamadas para o teste inspecionar depoisO teste precisa confirmar que uma interação aconteceu, sem configurá-la antes
FakeUma implementação menor e funcional do objeto realO teste precisa de comportamento real ao longo de várias chamadas, não um valor fixo
MockConstruído com expectativas sobre chamadas válidas, verificadas conforme acontecemO teste precisa verificar a forma exata de uma chamada: argumentos, quantidade, ordem

gateway foi um stub, um fake e um mock em três testes da mesma classe. O double segue o que o teste verifica, não a dependência.

Quando test doubles pioram os testes

Usar mock em todo colaborador acopla o teste a como o código é escrito, em vez do que ele faz. Adicione um teste que verifica que total_with_tax é chamado exatamente uma vez, depois refatore place_order para chamá-lo uma segunda vez só para registrar log: o teste quebra, e nada que importa para quem usa o sistema mudou. O teste com fake acima não tem esse problema, porque ele verifica o resultado (dois IDs de cobrança distintos, os valores certos registrados) em vez da forma da chamada.

Usar mock numa biblioteca que você não controla tem o mesmo problema um nível acima. Faça autospec direto de um cliente de terceiros e todo teste que o usa passa a depender das assinaturas de método dessa biblioteca; uma atualização pequena que renomeia um parâmetro quebra testes pela suíte inteira. Encapsule a biblioteca numa interface pequena sua e use o double nela. Quando a biblioteca mudar, só o wrapper muda.

Às vezes o número certo de test doubles é zero. Se a dependência real já é rápida e não tem efeitos colaterais, como uma função pura ou uma conexão SQLite em memória, criar um double adiciona indireção e não captura nada. Doubles servem para dependências lentas, remotas, não determinísticas, ou com efeitos colaterais que você não quer num teste.

Como escolher o test double certo

Comece pelo que o teste verifica, não pelo que parece mais seguro de substituir. Se você precisa de uma entrada previsível para uma asserção posterior funcionar, use stub. Para confirmar que o SMS foi enviado, use spy. Se a dependência precisa se comportar corretamente ao longo de várias chamadas, escreva um fake uma vez e reuse onde essa dependência aparecer. E quando um chamador passando os argumentos errados deve falhar o teste antes de falhar em produção, use um mock com autospec.

Nada disso funciona numa classe que constrói suas próprias dependências por dentro, porque não há onde encaixar um double. Esse é o problema que dependency injection resolve, e é a primeira coisa a corrigir se uma classe resiste a todo double deste artigo. Depois que a costura existe, decida qual double pertence a qual nível de teste. Quem trabalha test-first recorre a doubles desde o início, já que o teste existe antes de haver código real para chamar.

Artigos relacionados