Seus testes não deveriam ser DRY, mas sim WET

stephann

Stephann

Posted on September 26, 2023

Seus testes não deveriam ser DRY, mas sim WET

Bom, primeiro é bom deixar claro que o que vou escrever aqui não é nada novo, mas como comecei a aplicar esses conceitos nos últimos 2 anos nos projetos em que trabalhei e vi que tem sido realmente bem mais vantajoso do que a forma que eu fazia até então, resolvi escrever sobre. Se quiser ler/assistir outras pessoas falando sobre o assunto, seguem os links:

DRY x WET

DRY e WET são mais uns desses acrônimos que adoram colocar em princípios de desenvolvimento. DRY corresponde a Don't Repeat Yourself, traduzindo seria: Não se repita, que prega que você deve evitar ficar replicando um mesmo código em vários lugares da sua aplicação. Já o WET é o inverso disso, descreve códigos repetidos e pode ser traduzido como "Nós amamos digitar" (We Enjoy Typing) ou "Escreva tudo duas vezes/três vezes" (Write Everything Twice/Thrice).

O consenso é que DRY é bom, e WET é ruím. Isso prega na sua cabeça principalmente quando você está saindo do nível júnior e indo para o nível pleno. Não é incomum encontrar aqueles conteúdos "Entenda o conceito do SOLID e deixe de ser Jr. e suba de nível", e fala superficialmente que não repetir código é uma das habilidades necessárias para ser uma boa pessoa desenvolvedora.

Gosto de pensar que Júnior é o nível onde a pessoa aprende a desenvolver e consegue aplicar esses conhecimentos em aplicações reais, já o Pleno é o nível onde você entende e aplica de forma errada todos os princípios e boas práticas que colocam na sua frente, e você começa a se tornar senior quando vai aprendendo em quais contextos esses princípios são benéficos ou fazem mais mal do que bem.

O RSpec

Como o consenso é que DRY é bom, acabamos aplicando essa prática em todo o código, inclusive nos nossos testes, e o RSpec, uma das principais ferramentas de testes utilizadas pela comunidade Ruby, ajuda a perpetuar essa prática. Ele fornece vários mecanismos para secar seu código, evitando duplicações, alguns desses mecanismos são os #let, #subject, #before, #around, #after e os shared examples. Um exemplo de código:


describe Foo do
  describe "#bar" do
    subject(:foo) { Foo.new(x, y, z)

    let(:x) { 10 }
    let(:y) { 20 }
    let(:z) { x * y }
    let(:calc_result) { true }

    before do
      allow(Calculator).to receive(:calc)
        .with(z)
        .and_return(calc_result)
    end

    context "when x is greater than y" do
      let(:x) { 20 }
      let(:y) { 10 }

      it "returns something nice" do
        expect(foo.bar).to eq ...
      end
    end

    context "when y is greater than x" do
      it "returns something great" do
        expect(foo.bar).to eq ...
      end
    end

    context "when calc result returns false" do
      let(:calc_result) { false }

      it "..." do
        expect(foo.bar).to eq ...
      end
    end

    context "when calculator raises error" do
      before do
        allow(Calculator).to receive(:calc)
          .with(z)
          .and_raise("Some error")
      end

      it "..." { ... }
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Esse código está seguindo as boas práticas do DRY, e provavelmente se você não se incomodou com ele é porque você já deve estar acostumado a escrever código assim e ou acha que esse é a única forma de escrever testes com o RSpec.

Eu acredito que é um código problemático a longo prazo, pois você olha para um exemplo de teste (o it) e não consegue ver claramente como aquele teste está configurado, pois tem dependências no subject, nas variáveis let, nos before, e tudo isso pode ter sido ou não sobrescrito num escopo de context ou describe.

Então basicamente você precisa mapear mentalmente todo o arquivo de teste para entender como um teste está funcionando. Além disso, se você precisar modificar/adicionar um cenário, você sempre estará numa batalha entre adaptar as declarações existentes e quebrar os demais testes sem relação com sua atividade, e/ou acabar tendo que criar mais let/subject/before. Então o DRY nessa situação me parece uma furada.

Mas apesar de tudo, não é o RSpec o vilão, podemos aproveitar de vários benefícios da ferramenta mas sem utilizar as que consideramos problemáticas. Se a gente não quiser seguir o DRY, e deixar nosso código molhadinho, ele poderia ficar assim:

describe Foo do
  describe "#bar" do
    context "when x is greater than y" do
      it "returns something nice" do
        foo = Foo.new(20, 10, 200)

        expect(foo.bar).to eq ...
      end
    end

    context "when y is greater than x" do
      it "returns something great" do
        foo = Foo.new(10, 20, 200)

        expect(foo.bar).to eq ...
      end
    end

    context "when calc result returns false" do
      it "..." do
        foo = Foo.new(10, 20, 200)

        allow(Calculator).to receive(:calc)
          .with(200)
          .and_return(false)

        expect(foo.bar).to eq ...
      end
    end

    context "when calculator raises error" do
      it "..." do
        foo = Foo.new(10, 20, 200)

        allow(Calculator).to receive(:calc)
          .with(200)
          .and_raise("Some error")

        expect(foo.bar).to eq ...
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

O que eu fiz: Removi os before, os let e o subject, e deixei tudo que um teste precisa dentro dele próprio, ou seja, ele está contido e não tem nada externo que está influenciando na sua execução. Isso facilita a criação de novos testes e a adaptação de testes existentes, você olha para um exemplo e entende o que está acontecendo, sem surpresas.

Esse código até ficou com menos linhas do que o exemplo anterior, mas não é sobre isso que se trata essa prática, o normal ao aplicar essa prática é ter testes com mais linhas, e isso não pode ser encarado com algo negativo, eu penso que é melhor ter algo mais fácil de ler, entender e modificar mesmo que seja mais extenso do que ter um arquivo compacto que torna a vida do time de desenvolvimento mais difícil.

Enxugando um pouquinho

Claro, nem toda prática deve ser seguida cegamente, em 100% de todas as situações. Haverá situações onde reaproveitar código será a melhor saída do que replicar em todos os testes. A diferença é que você poderá fazer isso de forma discreta, apenas quando necessitar, não será sua atitude padrão para cada um dos testes. Por exemplo, em um teste de sistema você precisa reaproveitar uma série de passos que está se repetindo muito no arquivo, você pode criar um método Ruby comum ao invés de utilizar o before:

describe "Create product" do
  context "with valid form" do
    it "creates product" do
      store = create(:store)

      visit_new_product_page(store)

      fill_in "Title", "My product"
      ...
    end
  end

  context "with invalid form" do
    it "displays errors" do
      store = create(:store)

      visit_new_product_page(store)

      fill_in "Title", ""
      ...
    end
  end

  def visit_new_product_page(store)
    user = create(:user)
    user.add_role(:admin, store)

    sign_in user

    visit store_products_path(store)

    click_on "New product"
  end
end
Enter fullscreen mode Exit fullscreen mode

Você mais ranzinza pode estar pensando "é basicamente a mesma coisa de usar um before", mas é diferente, aqui você definiu um método auxiliar, ele só é executado se você precisar, no momento que você precisar, com os dados que você quer que ele receba, podendo retornar ou não os dados que você precisa, e se precisar de algo diferente você consegue criar um novo método com um novo comportamento.

Uma outra situação que acho tranquilo enxugar código: Você precisa de um mesmo objeto em testes simples e contidos:

describe Product do
  describe "validations" do
    subject(:product) { build(:product) }

    it { is_expected.to validate_presence_of(:title) } 
    it { is_expected.to validate_presence_of(:price) } 
    it { is_expected.to validate_uniqueness_of(:title) } 
    ... 
  end
end
Enter fullscreen mode Exit fullscreen mode

Ou seja, você vai sentir quando aplicar o DRY será vantajoso e quando repetir código estiver prejudicando a leitura daqueles testes. Então não é como se fosse proibido utilizar certas coisas, é como se agora você precisasse realmente de um motivo forte, e não só fazer porque foi assim que você aprendeu.

Conclusão

O código de teste deve ser considerado código da aplicação, mas é um código com um propósito diferente, e nem sempre as práticas utilizadas no código da regra de negócio podem ser vantajosas ao serem aplicadas nos testes.

Se você estiver fazendo uma nova aplicação, ou tem liberdade pra aplicar essa técnica numa aplicação existente, faça uma experiência e passe um tempo escrevendo testes WET. No início vai parecer que tem algo errado por conta da duplicação de código, mas depois você vai sentir a produtividade ao ter que adicionar e modificar testes existentes, sem precisar brigar com o RSpec e suas variáveis bagunçadas.

💖 💪 🙅 🚩
stephann
Stephann

Posted on September 26, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related