Ferramentas do comércio

Basicamente, os testes automatizados são apenas códigos que vão gerar ou causar um erro se algo estiver errado. A maioria das bibliotecas ou dos frameworks de teste oferece vários primitivos que facilitam a criação de testes.

Como mencionado na seção anterior, esses primitivos quase sempre incluem uma maneira de definir testes independentes (chamados de casos de teste) e fornecer declarações. Declarações são uma maneira de combinar a verificação de um resultado e gerar um erro se algo estiver errado, podendo ser consideradas o primitivo básico de todos os primitivos de teste.

Esta página aborda uma abordagem geral a esses primitivos. O framework escolhido provavelmente tem algo assim, mas essa não é uma referência exata.

Exemplo:

import { fibonacci, catalan } from '../src/math.js';
import { assert, test, suite } from 'a-made-up-testing-library';

suite('math tests', () => {
  test('fibonacci function', () => {
    // check expected fibonacci numbers against our known actual values
    // with an explanation if the values don't match
    assert.equal(fibonacci(0), 0, 'Invalid 0th fibonacci result');
    assert.equal(fibonacci(13), 233, 'Invalid 13th fibonacci result');
  });
  test('relationship between sequences', () => {
    // catalan numbers are greater than fibonacci numbers (but not equal)
    assert.isAbove(catalan(4), fibonacci(4));
  });
  test('bugfix: check bug #4141', () => {
    assert.isFinite(fibonacci(0)); // fibonacci(0) was returning NaN
  })
});

Este exemplo cria um grupo de testes (às vezes chamado de pacote) chamado "testes matemáticos" e define três casos de teste independentes, cada um executando algumas declarações. Esses casos de teste geralmente podem ser endereçados ou executados individualmente, por exemplo, por uma sinalização de filtro no executor de testes.

Auxiliares de declaração como primitivos

A maioria dos frameworks de teste, incluindo o Vitest, inclui uma coleção de auxiliares de declaração em um objeto assert que permitem verificar rapidamente os valores de retorno ou outros estados em relação a alguma expectation. Essa expectativa muitas vezes são valores "bons conhecidos". No exemplo anterior, sabemos que o 13o número de Fibonacci precisa ser 233, então podemos confirmar isso diretamente usando assert.equal.

Talvez você também espere que um valor tenha uma determinada forma, seja maior que outro valor ou tenha alguma outra propriedade. Este curso não abordará todos os possíveis auxiliares de declaração, mas os frameworks de teste sempre oferecem pelo menos as seguintes verificações básicas:

  • Uma verificação "verdadeira", geralmente descrita como "OK", verifica se uma condição é verdadeira, correspondendo a uma if que verifica se algo é bem-sucedido ou correto. Ele tende a ser fornecido como assert(...) ou assert.ok(...) e aceita um único valor além de um comentário opcional.

  • Uma verificação de igualdade, como no exemplo de teste matemático, em que você espera que o valor de retorno ou estado de um objeto seja igual a um bom valor conhecido. Eles são para igualdade primitiva (como para números e strings) ou igualdade referencial (sejam o mesmo objeto). Internamente, essas são apenas uma verificação "verdadeira" com uma comparação de == ou ===.

    • O JavaScript distingue entre igualdade flexível (==) e rígida (===). A maioria das bibliotecas de teste fornece os métodos assert.equal e assert.strictEqual, respectivamente.
  • Verificações de igualdade profunda, que estendem as verificações de igualdade para incluir a verificação do conteúdo de objetos, matrizes e outros tipos de dados mais complexos, além da lógica interna para transferir objetos e compará-los. Eles são importantes porque o JavaScript não tem uma maneira integrada de comparar o conteúdo de dois objetos ou matrizes. Por exemplo, [1,2,3] == [1,2,3] é sempre falso. Os frameworks de teste geralmente incluem os auxiliares deepEqual ou deepStrictEqual.

Os auxiliares de declaração que comparam dois valores (em vez de apenas uma verificação "verdadeira") costumam ter dois ou três argumentos:

  • O valor real, conforme gerado pelo código em teste ou que descreve o estado a ser validado.
  • O valor esperado, normalmente codificado (por exemplo, um número literal ou uma string).
  • Um comentário opcional descrevendo o que é esperado ou o que pode ter falhado, que será incluído se essa linha falhar.

Também é uma prática bastante comum combinar declarações para criar uma variedade de verificações, porque é raro que alguém possa confirmar corretamente o estado do sistema sozinho. Exemplo:

  test('JWT parse', () => {
    const json = decodeJwt('eyJieSI6InNhbXRob3Ii…');

    assert.ok(json.payload.admin, 'user should be admin');
    assert.deepEqual(json.payload.groups, ['role:Admin', 'role:Submitter']);
    assert.equal(json.header.alg, 'RS265')
    assert.isAbove(json.payload.exp, +new Date(), 'expiry must be in future')
  });

O Vitest usa a biblioteca de declaração do Chai internamente para fornecer auxiliares de declaração, e pode ser útil consultar a referência para conferir quais declarações e auxiliares podem ser adequados ao seu código.

Declarações fluentes e de BDD

Alguns desenvolvedores preferem um estilo de declaração que pode ser chamado de desenvolvimento orientado por comportamento (BDD, na sigla em inglês) ou declarações de estilo Fluent (em inglês). Eles também são chamados de auxiliares "esperar", porque o ponto de entrada para a verificação de expectativas é um método chamado expect().

É esperado que os auxiliares se comportem da mesma forma que as declarações escritas como chamadas de método simples, como assert.ok ou assert.strictDeepEquals, mas alguns desenvolvedores os consideram mais fáceis de ler. Uma declaração BDD pode ter a seguinte aparência:

// A failure here would generate "Expect result to be an array that does include 42"
const result = await possibleMeaningsOfLife();
expect(result).to.be.an('array').that.does.include(42);

// or a simpler form
expect(result).toBe('array').toContainEqual(42);

// the same in assert might be
assert.typeOf(result, 'array', 'Expected the result to be an array');
assert.include(result, 42, 'Expected the result to include 42');

Esse estilo de declarações funciona devido a uma técnica chamada encadeamento de métodos, em que o objeto retornado por expect pode ser continuamente encadeado com outras chamadas de método. Algumas partes da chamada, incluindo to.be e that.does no exemplo anterior, não têm função e são incluídas apenas para facilitar a leitura da chamada e gerar um comentário automatizado caso o teste falhe. É importante ressaltar que expect normalmente não oferece suporte a um comentário opcional, porque o encadeamento precisa descrever a falha com clareza.

Muitos frameworks de teste são compatíveis com declarações regulares e fluentes/BDD. O Vitest, por exemplo, exporta as duas abordagens de Chai e tem a própria abordagem um pouco mais concisa para BDD. Por outro lado, o Jest, por outro lado, inclui apenas um método esperado por padrão.

Agrupar testes em arquivos

Ao criar testes, já tendemos a fornecer agrupamentos implícitos. Em vez de todos os testes estarem em um só arquivo, é comum programar testes em vários arquivos. Na verdade, os executores de testes geralmente sabem que um arquivo é para teste devido a um filtro predefinido ou expressão regular. Por exemplo, o vitest inclui todos os arquivos no projeto que terminam com uma extensão como ".test.jsx" ou ".spec.ts" (".test" e ".spec", além de várias extensões válidas).

Os testes de componentes tendem a estar localizados em um arquivo de peering para o componente em teste, como na seguinte estrutura de diretórios:

Uma lista de arquivos em um diretório, incluindo gue.tsx e gue.test.tsx.
Um arquivo de componente e o arquivo de teste relacionado.

Da mesma forma, os testes de unidade tendem a ser colocados adjacentes ao código em teste. Cada teste completo pode ser colocado em um arquivo próprio, e os testes de integração podem até ser colocados em pastas exclusivas. Essas estruturas podem ser úteis quando casos de teste complexos crescem e exigem os próprios arquivos de suporte que não são de teste, como bibliotecas de suporte necessárias apenas para um teste.

Agrupar testes em arquivos

Conforme usado nos exemplos anteriores, é comum colocar testes dentro de uma chamada para suite(), que agrupa os testes configurados com test(). As suítes geralmente não são testes, mas ajudam a fornecer estrutura agrupando testes ou metas relacionados chamando o método aprovado. Para test(), o método transmitido descreve as ações do próprio teste.

Assim como nas declarações, há uma equivalência bastante padrão em fluent/BDD para testes de agrupamento. Alguns exemplos típicos são comparados no seguinte código:

// traditional/TDD
suite('math tests', () => {
  test('handle zero values', () => {
    assert.equal(fibonacci(0), 0);
  });
});

// Fluent/BDD
describe('math tests', () => {
  it('should handle zero values', () => {
    expect(fibonacci(0)).toBe(0);
  });
})

Na maioria dos frameworks, suite e describe se comportam de maneira semelhante, assim como test e it, em vez das maiores diferenças entre o uso de expect e assert para gravar declarações.

Outras ferramentas têm abordagens um pouco diferentes para organizar pacotes e testes. Por exemplo, o executor de testes integrado do Node.js é compatível com chamadas de aninhamento para test() a fim de criar implicitamente uma hierarquia de teste. No entanto, o Vitest só permite esse tipo de aninhamento usando suite() e não executará um test() definido dentro de outro test().

Assim como nas declarações, a combinação exata de métodos de agrupamento que seu conjunto de tecnologias oferece não é tão importante. Este curso os aborda no resumo, mas você precisa descobrir como eles se aplicam às suas ferramentas.

Métodos do ciclo de vida

Um motivo para agrupar os testes, mesmo implicitamente no nível superior de um arquivo, é fornecer métodos de configuração e desmontagem que sejam executados para cada teste ou uma vez para um grupo de testes. A maioria dos frameworks oferece quatro métodos:

Para cada `test()` ou `it()` Uma vez para a suíte
Antes da execução do teste "Antes de cada()" `beforeAll()`
Após execuções de teste "depois de cada()" "afterAll()"

Por exemplo, você pode querer preencher automaticamente um banco de dados virtual de usuários antes de cada teste e limpá-lo depois:

suite('user test', () => {
  beforeEach(() => {
    insertFakeUser('bob@example.com', 'hunter2');
  });
  afterEach(() => {
    clearAllUsers();
  });

  test('bob can login', async () => { … });
  test('alice can message bob', async () => { … });
});

o que pode ser útil para simplificar os testes. É possível compartilhar códigos comuns de configuração e eliminação em vez de duplicá-los em todos os testes. Além disso, se o próprio código de configuração e desmontagem gerar um erro, isso pode indicar problemas estruturais que não envolvem a falha dos testes.

Recomendações gerais

Aqui estão algumas dicas para lembrar ao pensar sobre esses primitivos.

Os primitivos são um guia

Lembre-se de que as ferramentas e primitivos aqui e nas próximas páginas não vão corresponder exatamente ao Vitest, Jest, Mocha, Web Test Runner ou qualquer outro framework específico. Embora tenhamos usado o Vitest como guia geral, mapeie-o para o framework de sua escolha.

Misture e combine declarações conforme necessário

Basicamente, testes são códigos que podem gerar erros. Cada executor fornecerá um primário, provavelmente test(), para descrever casos de teste distintos.

No entanto, se esse executor também fornecer assert(), expect() e auxiliares de declaração, lembre-se de que essa parte é mais sobre conveniência e você pode ignorá-la se precisar. Você pode executar qualquer código que possa gerar um erro, incluindo outras bibliotecas de declaração ou uma boa instrução if.

A configuração do ambiente de desenvolvimento integrado pode ajudar

Garanta que seu ambiente de desenvolvimento integrado, como o VSCode, tenha acesso ao preenchimento automático e à documentação nas ferramentas de teste escolhidas para aumentar a produtividade. Por exemplo, existem mais de 100 métodos em assert na biblioteca de declaração Chai, e a documentação do tipo correto aparece inline pode ser conveniente.

Isso pode ser especialmente importante para alguns frameworks de teste que preenchem o namespace global com os métodos de teste. Essa é uma diferença sutil, mas geralmente é possível usar bibliotecas de teste sem importá-las se elas forem adicionadas automaticamente ao namespace global:

// some.test.js
test('using test as a global', () => { … });

É recomendável importar os auxiliares mesmo que tenham suporte automático, porque isso dá ao ambiente de desenvolvimento integrado uma maneira clara de procurar esses métodos. Talvez você tenha enfrentado esse problema ao criar o React, já que algumas bases de código têm um React global mágico, mas outras não, e exigem que ele seja importado em todos os arquivos usando o React.

// some.test.js
import { test } from 'vitest';
test('using test as an import', () => { … });