Felipe Cesar

    Como remover código duplicado nos testes?

    É comum ver o uso de funções beforeEach e afterEach para acabar com a repetição de código nos testes, mas isso pode custar caro.

    describe("ShoppingCart", () => {
      let cart, el;
    
      beforeEach(() => {
        const container = document.createElement('div');
        cart = new ShoppingCart();
        renderCartDOM(container, cart);
        el = container.querySelector('#discountPercentage');
      });
    
      it("does not give a discount initially", () => {
        cart.addItem("Apple");
        expect(el.textContent).toBe('0');
      });
    
      it("gives a 5% discount when a book is added", () => {
        cart.addItem("Book");
        expect(el.textContent).toBe('5');
      });
    });

    Nesse caso, os testes parecem menores porque todo o código que estaria duplicado está dentro do beforeEach, o que parece ser o cenário ideal para tornar os testes legíveis e fáceis de manter.

    No início isso não parece tão ruim, mas essa estrutura começa a ficar complicada a medida que a quantidade de testes aumenta. Observe como o arquivo fica quando outros testes começam a ser adicionados.

    describe("ShoppingCart", () => {
      let cart, el;
    
      beforeEach(() => {
        const container = document.createElement('div');
        cart = new ShoppingCart();
        renderCartDOM(container, cart);
        el = container.querySelector('#discountPercentage');
      });
    
      describe("without books", () => {
        it("does not give a discount", () => {
          cart.addItem("Apple");
          expect(el.textContent).toBe('0');
        });
    
        it("does not give a discount when a book is removed", () => {
          cart.addItem("Apple");
          cart.addItem("Book");
          cart.removeItem("Book");
          expect(el.textContent).toBe('0');
        });
      });
    
      describe("with books", () => {
        it("gives a 5% discount", () => {
          cart.addItem("Book");
          expect(el.textContent).toBe('5');
        });
    
        it("keeps discount when at least one book is added", () => {
          cart.addItem("Book");
          cart.addItem("Book");
          cart.removeItem("Book");
          expect(el.textContent).toBe('5');
        });
      });
    });

    Agora faça o seguinte, observe apenas o último teste e responda a pergunta:

    • Onde o ShoppingCart foi declarado e instanciado?
    • Quantos beforeEach afetam esse teste?

    Você precisou fazer scroll para o topo do arquivo não é? Arquivos grandes nos fazem ficar dando scroll pra cima e pra baixo para entender o contexto dos testes. Isso nos faz perder muito tempo para ler e manter os testes, acaba se tornando uma tarefa bem cansativa.

    Lixeira dos testes

    Em alguns casos o beforeEach tende a ser a lixeira dos arquivos de teste. Pessoas jogam todo tipo de coisa lá: coisas que só são usadas em alguns testes, coisas que afetam todos os testes, e coisas que ninguém mais usa.

    Factory functions

    Factory functions são funções que nos ajudam a construir objetos ou estados e reusar a mesma lógica em diferentes lugares. Como ficariam os mesmos testes usando factory functions?

    describe("ShoppingCart", () => {
      function setup() {
        const container = document.createElement('div');
        const cart = new ShoppingCart();
        renderCartDOM(container, cart);
        const el = container.querySelector('#discountPercentage');
        return { cart, el };
      }
    
      describe("without books", () => {
        it("does not give a discount", () => {
          const { cart, el } = setup();
          cart.addItem("Apple");
          expect(el.textContent).toBe('0');
        });
    
        it("does not give a discount when a book is removed", () => {
          const { cart, el } = setup();
          cart.addItem("Apple");
          cart.addItem("Book");
          cart.removeItem("Book");
          expect(el.textContent).toBe('0');
        });
      });
    
      describe("with books", () => {
        it("gives a 5% discount", () => {
          const { cart, el } = setup();
          cart.addItem("Book");
          expect(el.textContent).toBe('5');
        });
    
        it("keeps discount when at least one book is added", () => {
          const { cart, el } = setup();
          cart.addItem("Book");
          cart.addItem("Book");
          cart.removeItem("Book");
          expect(el.textContent).toBe('5');
        });
      });
    });

    O tamanho do código é praticamente o mesmo, mas o código é mais legível. Eliminamos o beforeEach mas o código não perdeu nada em manutenibilidade. A quantidade de repetição que nós eliminamos não mudou muito, mas a legibilidade melhorou bastante. Não precisamos de tantos scrolls para entender onde e como um objeto foi criado, e fica bem claro quando ele foi criado e os parâmetros que foram passados na criação.

    O describe é legal para criar um contexto, mas cria um aninhamento no código que pode dificultar a leitura, nesse caso podemos remove-lo do código.

    function setup() {
      const container = document.createElement('div');
      const cart = new ShoppingCart();
      renderCartDOM(container, cart);
      const el = container.querySelector('#discountPercentage');
      return { cart, el };
    }
    
    test("does not give a discount", () => {
      const { cart, el } = setup();
      cart.addItem("Apple");
      expect(el.textContent).toBe('0');
    });
    
    test("does not give a discount when a book is removed", () => {
      const { cart, el } = setup();
      cart.addItem("Apple");
      cart.addItem("Book");
      cart.removeItem("Book");
      expect(el.textContent).toBe('0');
    });
    
    test("gives a 5% discount", () => {
      const { cart, el } = setup();
      cart.addItem("Book");
      expect(el.textContent).toBe('5');
    });
    
    test("keeps discount when at least one book is added", () => {
      const { cart, el } = setup();
      cart.addItem("Book");
      cart.addItem("Book");
      cart.removeItem("Book");
      expect(el.textContent).toBe('5');
    });