Artigo original: How to Start Unit Testing Your JavaScript Code

Todos sabemos que devemos escrever testes unitários. O difícil, no entanto, é saber onde começar e quanto tempo dedicar aos testes em comparação com a implementação de fato. Então, onde começar? A testagem tem a ver apenas com o código ou os testes unitários apresentam outros benefícios?

Neste artigo, explicarei os tipos diferentes de testes e quais benefícios o teste unitários traz às equipes. Também apresentarei o Jest - um framework de testes de JavaScript.

Tipos diferentes de teste

Antes de entrarmos nas especificidades dos testes unitários, gostaria de analisar rapidamente os tipos diferentes de testes. Frequentemente, existem algumas confusões a respeito deles e eu não estou surpreso. Por vezes, a linha entre eles é bem tênue.

Testes unitários

Os testes unitários verificam apenas uma única parte de sua implementação - uma unidade, por assim dizer. Nada de verificar dependências ou integrações, nada de ver especificidades de um framework. São como um método que retorna um link em um linguagem específica:

export function getAboutUsLink(language){
  switch (language.toLowerCase()){
    case englishCode.toLowerCase():
      return '/about-us';
    case spanishCode.toLowerCase():
      return '/acerca-de';
  }
  return '';
}

Testes de integração

Em algum momento, seu código se comunica com um banco de dados, um sistema de arquivos ou com terceiros. Esse "terceiro" pode ser até mesmo um outro módulo da sua aplicação.

Essa parte da implementação deve ser testada pelos testes de integração. Eles têm tipicamente uma configuração mais complicada, que envolve a preparação de ambientes de teste, inicialização de dependências e assim por diante.

Testes funcionais

Os testes unitários e os testes de integração dão a você a confiança de sua aplicação funciona. Testes funcionais examinam a aplicação do ponto de vista do usuário e testam se o sistema está funcionando conforme o esperado.

presentation

No diagrama acima, você vê que os testes unitários são a base maior do conjunto de testes de sua aplicação. Tipicamente, eles são pequenos, existem muitos deles e eles são executados automaticamente.

Agora, vamos ver os testes unitários em detalhes.

Por que eu deveria me importar de escrever testes unitários?

Sempre que peço aos desenvolvedores para que escrevam testes para suas aplicações, a resposta é: "eu não tive tempo para os testes" ou "não preciso de testes, sei que funciona."

É aí que eu sorrio gentilmente e digo a eles o que estou prestes a dizer para vocês. Os testes unitários não têm a ver com testagem apenas. Eles ajudam você de outras formas, também, para que você possa:

Ter a confiança no fato de que seu código funciona. Quando foi a última vez que você fez o commit de uma alteração no código, houve uma falha no build, e metade da sua aplicação parou de funcionar? No meu caso, na semana passada.

Mas isso ainda está bem. O problema real é quando há sucesso na build, a alteração é implantada e sua aplicação começa a ficar instável.

Quando isso acontece, você começa a perder confiança no seu código, até que chega o momento em que você simplesmente reza para a aplicação funcionar. Testes unitários ajudarão você a descobrir problemas mais cedo e a ter mais confiança.

Tomar decisões melhores sobre a arquitetura. O código muda, mas algumas decisões sobre a plataforma, módulos, estrutura e outras precisam ser feitas durante os estágios iniciais de um projeto.

Quando você começa a pensar nos testes unitários desde o princípio, isso ajudará você a estruturar melhor o seu código e a conseguir uma separação das responsabilidades adequada. você não estará tentado a atribuir diversas responsabilidades para blocos de código únicos pois estes seriam um pesadelo para os testes unitários.

Defina a funcionalidade antes de programar. você escreve a assinatura do método e começa a implementar imediatamente. Ah, mas o que aconteceria no caso de um parâmetro ser nulo? E se o valor estivesse fora do intervalo esperado ou se tivesse muitos caracteres? Você lançaria uma exceção ou retornaria nulo?

Os testes unitários ajudarão você a descobrir todos esses casos. Examine as questões novamente que você descobrirá exatamente o que define seus casos de teste para os testes unitários.

Tenho certeza de que há muito mais benefícios na hora de escrever testes unitários. Estes são apenas alguns dos quais eu me lembro em função da experiência. Foram aqueles que eu aprendi do jeito mais difícil.

Como escrever seu primeiro teste unitário em JavaScript

Vamos, no entanto, voltar ao JavaScript. Começaremos com o Jest, que é um framework de testes do JavaScript. Ele é uma ferramenta que permite o teste unitário automático, Proporciona cobertura do código e permite criar objetos do tipo mock facilmente. O Jest também tem uma extensão para o Visual Studio Code, disponível aqui.

Também existem outros frameworks. Se você estiver interessado, pode conferi-los neste artigo (texto em inglês).

npm i jest --save-dev

Vamos usar o método mencionado anteriormente, getAboutUsLink, como uma implementação o que queremos testar:

const englishCode = "en-US";
const spanishCode = "es-ES";
function getAboutUsLink(language){
    switch (language.toLowerCase()){
      case englishCode.toLowerCase():
        return '/about-us';
      case spanishCode.toLowerCase():
        return '/acerca-de';
    }
    return '';
}
module.exports = getAboutUsLink;

Coloco isso no arquivo index.js. Podemos escrever testes no mesmo arquivo, mas é uma boa prática separar os testes unitários em um arquivo dedicado.

Os padrões de nomeação comuns incluem {nome_do_arquivo}.test.js e {nome_do_arquivo}.spec.js. Usei o primeiro, index.test.js:

const getAboutUsLink = require("./index");
test("Returns about-us for english language", () => {
    expect(getAboutUsLink("en-US")).toBe("/about-us");
});

Primeiro precisamos importar a função que queremos testar. cada teste é definido como uma invocação da função test. O primeiro parâmetro é o nome do teste para sua referência. o outro é uma arrow function, onde chamamos a função que queremos testar e especificamos quais resultados esperamos.

Neste caso, chamamos a função getAboutUsLink com en-US como parâmetro de linguagem. Esperamos que o resultado seja /about-us.

Agora, podemos instalar a CLI do Jest globalmente e executar o teste:

npm i jest-cli -g
jest

Se você ver um erro relacionado à configuração, certifique-se de que o arquivo package.json está presente. Caso não esteja, gere um usando npm init.

Você deverá ver algo assim:

 PASS  ./index.test.js
  √ Returns about-us for english language (4ms)
  console.log index.js:15
    /about-us
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.389s

Ótimo! Esse foi o primeiro teste unitário simples em JavaScript, do começo ao fim. Se você instalou a extensão do Visual Studio Code, ela executará os testes automaticamente quando você salvar um arquivo. Vamos tentar isso estendendo o teste com a linha abaixo:

expect(getAboutUsLink("cs-CZ")).toBe("/o-nas");

Ao salvar o arquivo, o Jest informará a você que o teste falhou. Isso ajuda você a descobrir problemas em potencial mesmo antes de enviar suas alterações para um commit.

Teste de funcionalidades avançadas e fazendo o mock de serviços

Na vida real, os códigos de idiomas para o método getAboutUsLink não seriam constantes no mesmo arquivo. Seu valor é tipicamente usado durante o projeto. Assim, eles seriam definidos em seu próprio módulo e importados em todas as funções que os utilizassem.

import { englishCode, spanishCode } from './LanguageCodes'

Você pode importar essas constantes nos testes da mesma maneira. No entanto, a situação ficará mais complicada se você estiver trabalhando com objetos em vez de constantes simples. Vamos dar uma olhada nesse método:

import { UserStore } from './UserStore'
function getUserDisplayName(){
  const user = UserStore.getUser(userId);
  return `${user.LastName}, ${user.FirstName}`;
}

O método usa a UserStore importada:

class User {
    getUser(userId){
        // lógica para obter dados de um banco de dados
    }
    setUser(user){
        // lógica para armazenar dados em um banco de dados
    }
}
let UserStore = new User();
export { UserStore }

Para fazer o teste unitário desse método adequadamente, precisamos do mock de UserStore. Um mock é um substituto do objeto original. Ele nos permite separar as dependências e os dados reais da implementação dos métodos testados, como manequins de testes ajudam nos testes de colisão de carros, no lugar de pessoas reais.

Se não usássemos o mock, testaríamos essa função e o armazenamento. Esse seria um teste de integração e provavelmente precisaríamos do mock do banco de dados utilizado.

Mock de um serviço

Para fazer o mock de objetos, você pode fornecer uma função para criar o mock ou fazer o mock manualmente. Vou me concentrar na segunda, pois o caso de uso é direto e simples. Fique à vontade, no entanto, para conferir as outras possibilidades de mock que o Jest oferece (texto em inglês).

jest.mock('./UserStore', () => ({
    UserStore: ({
        getUser: jest.fn().mockImplementation(arg => ({
            FirstName: 'Ondrej',
            LastName: 'Polesny'
        })),
        setUser: jest.fn()
    })
}));

Primeiro, precisamos especificar qual mock estamos fazendo - do módulo ./UserStore. Em seguida, precisamos retornar o mock que contém todos os objetos exportados daquele módulo.

Nesta amostra, somente o objeto User chamado de UserStore com a função getUser. Porém, com implementações reais, o mock pode ser muito maior. Todas as funções com as quais você não se importa muito e que estão no escopo do teste unitário podem ser replicadas com facilidade com o jest.fn().

O teste unitário para a função getUserDisplayName é semelhante ao que criamos antes:

test("Returns display name", () => {
    expect(getUserDisplayName(1)).toBe("Polesny, Ondrej");
})

Assim que eu salvo o arquivo, o Jest me informa que tenho 2 testes passando. Se você estiver executando os testes manualmente, faça isso agora e certifique-se de que está vendo o mesmo resultado.

Relatório da cobertura do código

Agora que sabemos como testar o código em JavaScript, é bom cobrir tanto código quanto for possível com testes. Isso é difícil de se fazer. No fim, somos apenas pessoas. Queremos nossas tarefas concluídas e os testes unitários geralmente acrescentam uma carga de trabalho desnecessária, que temos a tendência de desconsiderar. A cobertura do código é uma ferramenta que nos ajuda a combater isso.

A cobertura de código informará a você a porção de seu código já coberta por testes unitários. Veja, por exemplo, meu primeiro teste unitário verificando a função getAboutUsLink:

test("Returns about-us for english language", () => {
   expect(getAboutUsLink("en-US")).toBe("/about-us");
});

Ela confere o link em inglês, mas a versão em espanhol não é testada. A cobertura do código é de 50%. O outro teste unitário verifica a função getDisplayName em sua totalidade. Sua cobertura de código é de 100%. Juntos, a cobertura de código total é de 67%. Temos 3 casos de uso a serem testados, mas nossos testes cobriram apenas 2 deles.

Para ver o relatório de cobertura de código, digite o comando a seguir no terminal:

jest --coverage

Se, no entanto, você estiver usando o Visual Studio Code com a extensão do Jest, você pode executar o comando (CTRL+SHIFT+P) Jest: Toggle Coverage Overlay. Ele mostrará a você, diretamente na implementação, quais linhas de código não estão cobertas por testes.

code-coverage-inline

Ao executar a verificação de cobertura, o Jest também criará um relatório em HTML. Você o encontrará na sua pasta do projeto, em coverage/lcov-report/index.html.

code-coverage

Não sei se preciso mencionar isso, mas você deve buscar sempre 100% de cobertura do código, está bem? :-)

Resumo

Neste artigo, mostrei a você como começar com os testes unitários em JavaScript. Embora seja ótimo que sua cobertura de código chegue aos 100% no relatório, na realidade, nem sempre é possível chegar lá (e fazer com que os testes façam sentido). O objetivo é deixar os testes unitários ajudarem você a manter seu código e a garantir que ele sempre funcione como o pretendido. Eles ajudam você a:

  • definir com clareza os requisitos de implementação,
  • projetar melhor o seu código e separar as responsabilidades,
  • descobrir os problemas que você pode introduzir com seus novos commits,
  • dar a você mesmo a confiança de que seu código funciona.

O melhor lugar para se iniciar é na página Getting started da documentação do Jest (em inglês), para que você possa experimentar essas práticas por sua conta.

Você tem alguma experiência própria com a testagem de código? Eu adoraria saber a respeito. Fale-me sobre ela pelo Twitter ou participe das minhas streams no Twitch.