DEV Community

Cover image for Introdução a Testes de Unidade com Javascript 📘
Ágatha
Ágatha

Posted on • Updated on

Introdução a Testes de Unidade com Javascript 📘

Testes Automatizados 🤖

Fazer testes manualmente, ou seja, uma pessoa sentar e testar tudo passo a passo, leva muito tempo, custa caro e é um trabalho que ninguém quer fazer. Além disso, sempre que algo no programa muda, tem que testar tudo de novo…
Para entender melhor os testes automatizados, existe uma coisa chamada pirâmide de testes, que foi uma ideia do Mike Cohn.
Imagine uma pirâmide dividida em partes, onde cada parte mostra um tipo diferente de teste, organizados do mais simples e rápido lá embaixo até os mais complexos e demorados no topo. Essa pirâmide ajuda a entender que não todos os testes são iguais: alguns são básicos e outros são bem detalhados, mas todos são importantes para garantir que o programa funcione direitinho.

Imagem do livro Engenharia de Software Moderna. Disponível em: https://engsoftmoderna.info/figs/cap8/piramide-testes.svg


  1. Na base da pirâmide, temos os testes de unidade. Eles são como checar as peças de um quebra-cabeça uma por uma para ter certeza de que cada uma está certa. Cada teste desses olha para uma pequena parte do programa (geralmente uma única função ou classe) para ver se ela está fazendo o que deveria. Esses testes são muitos, rápidos de rodar e fáceis de fazer.

Image description


  1. No meio da pirâmide, encontramos os testes de integração ou testes de serviços. Imagine tentar ver se algumas peças do quebra-cabeça se encaixam como deveriam. Estes testes olham como diferentes partes do programa trabalham juntas. Por exemplo, eles podem testar se o programa salva as informações certinho no banco de dados. Esses testes são um pouco mais complexos que os testes de unidade e demoram mais para serem feitos e rodados.

Image description


  1. No topo da pirâmide, temos os testes de sistema ou testes de interface com o usuário. Isso é como pegar o quebra-cabeça montado e ver se a imagem final está certa. Estes testes verificam o programa inteiro, de ponta a ponta, do jeito que um usuário de verdade usaria. São os testes mais complexos, mais demorados para rodar e custam mais caro. Eles também são "frágeis", o que quer dizer que pequenas mudanças no programa podem fazer com que esses testes precisem ser ajustados.

Image description


Uma recomendação genérica é que esses três testes sejam implementados na seguinte proporção: 70% como testes de unidades; 20% como testes de serviços e 10% como testes de sistema.


Testes de Unidade 🧪

Os testes de unidade são como mini-testes para pedacinhos do seu programa. Imagine que seu programa é um robô, e cada teste de unidade verifica se uma parte específica do robô (como um braço ou uma roda) está funcionando direitinho. Você escreve um pequeno teste para cada classe do seu programa, que é basicamente um conjunto de instruções que o programa segue. Cada teste verifica se, ao pedir para essa classe fazer alguma coisa (como somar dois números), ela realmente faz isso certo.

Então, quando você olha para o seu programa, você tem duas grandes partes: as classes, que são como o coração do programa, fazendo todo o trabalho; e os testes de unidade, que são como um checklist para garantir que cada parte do programa está fazendo seu trabalho corretamente. É como se tivesse um manual de "como as coisas deveriam funcionar" e estivesse marcando cada item para ter certeza de que está tudo ok.

Image description

A figura ilustra um sistema com várias classes, que são como as diferentes partes de um programa, e os testes correspondentes a essas classes. Algumas classes têm mais de um teste associado a elas, o que geralmente acontece porque essas partes do programa são mais complexas ou críticas, então precisam ser testadas em mais situações para ter certeza de que estão sempre funcionando bem.

Por exemplo, a classe C1 é testada tanto por T1 quanto por T2, talvez ela tem um papel importante no programa e precisa ser muito bem verificada. Já a classe C2 não tem nenhum teste associado, o que pode significar que os desenvolvedores ainda não fizeram testes para ela ou talvez ela não seja tão essencial assim para o funcionamento geral do sistema.

Os testes de unidade são criados com a ajuda de ferramentas especiais chamadas frameworks xUnit. Esses frameworks são como kits de ferramentas que ajudam a criar e rodar os testes automaticamente. O "x" no xUnit é substituído pelo nome da linguagem de programação usada. Atualmente já existem versões de frameworks xUnit para as principais linguagens de programação.

Uma das vantagens de testes de unidade é que não é necessário aprender uma nova linguagem, por que os testes são implementados na mesma linguagem do sistema que se pretende testar. Assim, os desenvolvedores podem se concentrar em melhorar o programa, sem ter que gastar tempo extra aprendendo outras coisas só para testá-lo.

Para explicar os conceitos de testes de unidade, vamos usar uma classe Stack:

class Stack {
  constructor() {
    this.elements = [];
  }

  size() {
    return this.elements.length;
  }

  isEmpty() {
    return this.elements.length === 0;
  }

  push(elem) {
    this.elements.push(elem);
  }

  pop() {
    if (this.isEmpty())
      throw new Error('Stack is empty');
    return this.elements.pop();
  }
}

module.exports = Stack
Enter fullscreen mode Exit fullscreen mode
const Stack = require('./stack');

describe('Stack', () => {
  let stack;

  beforeEach(() => {
    stack = new Stack();
  });

  test('pushes an element onto the stack', () => {
    stack.push(1);
    expect(stack.size()).toBe(1);
  });

  test('pops an element from the stack', () => {
    stack.push(2);
    expect(stack.pop()).toBe(2);
    expect(() => stack.pop()).toThrow('Stack is empty');
  });

  test('checks if the stack is empty', () => {
    expect(stack.isEmpty()).toBeTruthy();
  });
});
Enter fullscreen mode Exit fullscreen mode
  • Usamos describe para agrupar testes relacionados a uma certa funcionalidade ou classe.
  • beforeEach é uma função que é executada antes de cada teste, configurando um ambiente limpo. Neste caso, estamos criando uma nova instância da pilha antes de cada teste.
  • test (ou it em algumas frameworks) é usado para definir um teste individual.
  • expect é uma função que verifica se o resultado corresponde ao esperado. Se não corresponder, o teste falha.

Image description


Para concluir, vamos mostrar o código completo do teste de unidade:

const Stack = require('./stack'); 
describe('Stack', () => {
  let stack;

  beforeEach(() => {
    stack = new Stack();
  });

  test('new stack should be empty', () => {
    // Verifica se a nova pilha está vazia.
    expect(stack.isEmpty()).toBeTruthy();
  });

  test('stack with one element should not be empty', () => {
    // Verifica se a pilha não está vazia após adicionar um elemento.
    stack.push(10);
    expect(stack.isEmpty()).toBeFalsy();
  });

  test('stack should have correct size after pushing elements', () => {
    // Verifica se o tamanho da pilha está correto após adicionar elementos.
    stack.push(10);
    stack.push(20);
    stack.push(30);
    expect(stack.size()).toBe(3);
  });

  test('stack should pop elements in the correct order', () => {
    // Verifica se os elementos são retirados da pilha na ordem correta.
    stack.push(10);
    stack.push(20);
    stack.push(30);
    let result = stack.pop();
    expect(result).toBe(30); // O último elemento empilhado é o primeiro a ser retirado
    result = stack.pop();
    expect(result).toBe(20);
  });

  test('pop should throw an error on empty stack', () => {
    // Verifica se uma exceção é lançada ao retirar um elemento de uma pilha vazia.
    stack.push(10);
    stack.pop(); // Deve funcionar bem
    expect(() => stack.pop()).toThrow('Stack is empty'); // Espera-se um erro aqui
  });

});

Enter fullscreen mode Exit fullscreen mode
  • Cada função test verifica uma afirmação específica sobre a classe Stack.
  • A função expect é seguida por uma "matcher function" (toBeTruthy, toBeFalsy, toBe, toThrow), que define o resultado esperado.
  • A cláusula toThrow é usada para verificar se a função lança um erro quando tentamos fazer pop em uma pilha vazia.

Image description


Definições 📚

Mas o que de fato significa essas definições que aparecem quando rodam os testes, como "Test Suites"?

  1. Teste: É como um mini-desafio que o programa precisa passar. No mundo dos testes, é um pedacinho de código que vai verificar se outra parte do programa faz o que deveria fazer.
  2. Fixture: É a preparação, o que você faz antes de começar. Imagine que você precisa que algumas coisas estejam sempre no lugar certo antes de começar um teste. Por exemplo, se você vai testar se um carro anda, você precisa de um carro com motor, rodas e combustível. Isso é o fixture: o conjunto de coisas que você prepara para poder realizar seus testes.
  3. Casos de Teste (Test Case): Isso é como uma coleção de mini-desafios relacionados. É uma classe que tem vários métodos de teste dentro dela. Você pode pensar nela como uma lista de verificações que seu programa precisa passar.
  4. Suíte de Testes (Test Suite): Quando você tem várias coleções de mini-desafios (casos de teste) e quer passar por todas elas de uma vez, isso se chama suíte de testes. É um pacote que junta muitos testes diferentes para rodar todos juntos.
  5. Sistema sob Teste (System Under Test, SUT): É o próprio programa que você está testando. Assim como numa peça de teatro, o "sistema sob teste" é o ator principal, aquele que está sob os holofotes e sendo avaliado para ver se faz tudo certinho. Em outras palavras, é o código real que as pessoas vão usar.

Pensando num exemplo prático: se você tivesse um aplicativo de lista de tarefas, os "testes" seriam os códigos que verificam se você pode adicionar e remover tarefas corretamente, a "fixture" seria a lista inicial que você configura para os testes, os "casos de teste" seriam todas as verificações relacionadas à lista de tarefas, o "suíte de testes" seria um conjunto completo de testes não só da lista de tarefas, mas talvez também de outras partes do aplicativo, e o "SUT" seria o aplicativo de lista de tarefas inteiro que você está testando.

Todas as imagens de explicação de pirâmide de testes foram tiradas do livro eng de software moderna. Disponível em: https://engsoftmoderna.info/cap8.html


A maior parte desse conteúdo veio do livro "Engenharia de Software Moderna", escrito por Marco Tulio Valente. Esse livro é um tesouro cheio de dicas sobre como criar testes do jeito certo, usando o que tem de mais novo e eficiente no mundo da programação. Entretanto, a única diferença, é que o livro é em Java e aqui eu adaptei para utilizar Javascript.


Top comments (0)