Um bom teste conta uma história. Isso pode servir não só para garantir que o software está se comportando da maneira esperada, mas também como uma documentação para outros desenvolvedores.
Frequentemente, eu vejo este tipo de teste:
before do
model_a = create(:model_a, status: :draft)
model_b = create(:model_b, created_at: Date.today)
model_a.run_operation(with: model_b)
end
# N linhas de teste aqui...
describe("ModelA") do
before do
double(:external_system_1, ...)
double(:external_system_2, ...)
end
it("executou o método com sucesso") do
expect(model_a.valid).to be_true
end
it("está associada à ModelB") do
expect(model_a.relations).to contain(ModelB)
end
}
Algumas coisas são testadas em um objeto onde cada teste precisa de um setup, uma configuração, igual ou semelhante. Um dos primeiros conselhos que recebemos como programadores é não escrever código duplicado: Não se repita (ou DRY1, se preferir). Reescrever blocos de código idênticos para configurar um teste com certeza parece ser um caso claro de repetição, e normalmente o que nós fazemos é extrair essa lógica para um bloco before
.
Isso é um erro quando se trata de testes.
Otimize seus testes para Legibilidade, não para Brevidade
Brevidade no primeiro momento parece ser uma otimização. Essas linhas extras configurando o teste estão fazendo cada teste para a ModelA 75% menores! Mas a desvantagem é que para entender o que é exigido da ModelA, você precisa procurar por todo o arquivo. Isso torna difícil de lembrar toda a configuração necessária para fazer uma Model válida.
Isso é como pegar um romance e juntar toda a ambientação da história e a construção dos personagens em um único e grande capítulo. E depois, escrever uma série de capítulos de uma só frase onde cada personagem tem um momento de recompensa, ou a história chega a um clímax dramático.
Reutilizar várias e várias vezes uma mesma configuração faz parecer que estamos quebrando todas as regras. Por mais contra-intuitivo que possa ser, a melhor maneira de tornar um teste legível é colocar todo o contexto próximo à expectativa, todas as vezes. Se isso significa repetir toneladas de código… Tudo bem! Afinal, o tempo que leva para entender o código é caro, e adicionar linhas de código a um arquivo de texto é muito barato.
Aqui temos um exemplo real de um repositório interno da thoughtbot:
describe Person, ".with_downtime_next_week" do
it "returns people with downtime" do
inactive_person = create(:person, name: "exclude inactive person")
unbillable_person = create(:person, name: "exclude unbillable person")
active_billable_unbooked_person = create(
:person,
name: "include person with downtime",
)
vacation_person = create(:person, name: "exclude vacation person")
create(
:position,
person: inactive_person,
ends_on: 2.months.ago,
)
create(
:position,
person: unbillable_person,
department: create(:department, billable: false),
)
create(
:position,
person: active_billable_unbooked_person,
department: create(:department, billable: true),
starts_on: 1.month.ago,
)
create(
:position,
person: vacation_person,
department: create(:department, billable: true),
starts_on: 1.month.ago,
)
create(
:reservation,
person: vacation_person,
starts_on: Date.current.next_week,
ends_on: 3.months.from_now,
)
people = Person.with_downtime_next_week.pluck(:name)
expect(people).to contain_exactly("include person with downtime")
end
end
Ao todo, esse arquivo contém mais 88 testes, muitos dos quais também criam pessoas e as vinculam a outros objetos. Há muitas oportunidades para extrair funcionalidades comuns aqui. Mas fazendo isso, estaríamos arriscando de torná-lo mais difícil de entender.
Um bom teste conta uma história. Pelo fato dele ser auto-explicativo, a história dele se torna fácil de ler:
- Onde haja uma pessoa inativa,
- uma pessoa com horas não-faturáveis,
- uma pessoa ativa e com horas faturáveis que está sem datas reservadas
- e uma pessoa de férias.
- O método
with_downtime_next_week
irá apenas retornar a pessoa ativa e com horas faturáveis que está sem datas reservadas.
Essa história não nos tornará um(a) “Escritor(a) Best-Seller”, mas é muito útil para alguém tentando entender os detalhes do método with_downtime_next_week
. Esse teste também nos mostra como usar outras Models (positions
e reservations
) para configurar diferentes tipos de pessoas. Essa configuração extra pode dar a sensação de que irá nos distrair, mas também pode ser o diferencial para alguém que precisa descobrir como corrigir um bug ou escrever uma funcionalidade.
Quando Devemos Extrair as Configurações
Em alguns casos, extrair a configuração não é só uma boa ideia, mas é preferível. Seu teste pode precisar de um banco de dados para ser executado. Ou talvez algumas factories para gerar esses dados. Faça isso fora do seu teste.
A diferença aqui é que elas são preocupações que não fazem parte da lógica de negócios que você está testando. Qualquer coisa relacionada à configuração do seu ambiente ou a uma solução alternativa necessária para executar testes em um ambiente separado deve ficar fora dos testes.
Encontrando um Ponto de Equilíbrio
Às vezes, a configuração involve realmente muitas linhas de código. Nesses casos, um meio termo seria extrair as menores partes possíveis, transformando-as em funções, e chamá-las no seu teste. Um exemplo:
describe ApiRequest do
it "calls the api with arguments" do
api_request = mock_web_request(
url: "https://...",
verb: :get,
params: {...}
# muitas linhas complicadas
# especificando parâmetros complicados
# com returns complicados
)
expect(api_request).to have_received(...)
end
end
Essa não é uma configuração exatamente complexa, mas sim longa. Às vezes, a configuração necessária pode não ser tão descritiva quanto gostaríamos. Considere a seguinte refatoração:
describe ApiRequest do
it "calls the api with arguments" do
params = {...}
api_request = user_data_api_call(params)
expect(api_request).to have_received(...)
end
def user_data_api_call(params)
mock_web_request(
url: "https://...",
verb: :get,
params: params
# muitas linhas complicadas
# especificando parâmetros complicados
# com returns complicados
)
end
end
Agora o método user_data_api_call
mantém o seu teste mais legível, sem esconder o fato de que existe alguma configuração. Nós sabemos qual é essa configuração e onde encontrá-la, ao invés de escondê-la em um bloco before
. Esses métodos extraídos ainda podem tornar um teste complicado e difícil de entender. Portanto, faça isso com moderação. Mas quando as extrações são bem feitas, essas funções menores e mais focadas podem melhorar a legibilidade do teste. Neste exemplo, eles podem até mesmo dar um nome mais descritivo à requisição web que foi simulada!
Uma consideração importante a se fazer é que se a configuração do teste for genuinamente desgastante, pode ser um sinal de que também é desgastante para seus usuários. Também pode ser um sinal de que seu código está altamente acoplado e mal fatorado. Vale a pena ficar de olho nisso à medida que seu sistema cresce, mas isso é assunto para outro momento.
Da próxima vez que você estiver fazendo a mesma configuração repetidamente nos seus testes, pense nisso como se estivesse escrevendo uma história, um pedaço de uma prosa para um futuro desenvolvedor. Esse futuro desenvolvedor pode até ser você, e eles vão agradecer por isso.
Leituras Adicionais
-
Don’t Repeat Yourself, do inglês ↩