O Teste Auto-Explicativo

Edward Loveall
Traduzido por Fernando Marques

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


  1. Don’t Repeat Yourself, do inglês