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.


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

About thoughtbot

We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.