Herramientas del oficio

Las pruebas automatizadas, en esencia, son solo código que arrojará o causará un error si algo anda mal. La mayoría de las bibliotecas o los frameworks de prueba proporcionan una variedad de primitivas que facilitan la escritura de las pruebas.

Como se mencionó en la sección anterior, estas primitivas casi siempre incluyen una forma de definir pruebas independientes (denominadas casos de prueba) y de proporcionar aserciones. Las aserciones son una forma de combinar la verificación de un resultado y arrojar un error si algo no funciona, y pueden considerarse las primitivas básicas de todas las primitivas de prueba.

En esta página, se brinda un enfoque general a estas primitivas. Es probable que el framework que elijas tenga algo como esto, pero esta no es una referencia exacta.

Por ejemplo:

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
  })
});

En este ejemplo, se crea un grupo de pruebas (a veces llamada paquete) llamado "pruebas matemáticas" y se definen tres casos de prueba independientes en los que cada una ejecuta algunas aserciones. Por lo general, estos casos de prueba se pueden abordar de forma individual o ejecutarse, por ejemplo, con una marca de filtro en el ejecutor de pruebas.

Asistentes de aserción como primitivas

La mayoría de los frameworks de prueba, incluido Vitest, incluyen una colección de asistentes de aserción en un objeto assert que te permiten verificar rápidamente los valores que se muestran y otros estados en función de alguna expectation. Esa expectativa suele ser valores "bien conocidos". En el ejemplo anterior, sabemos que el número 13 de Fibonacci debe ser 233, por lo que podemos confirmarlo directamente con assert.equal.

Es posible que también esperes que un valor toma una determinada forma, que es mayor que otro valor o que tiene alguna otra propiedad. En este curso, no se cubrirá el rango completo de posibles asistentes de aserción, pero los frameworks de prueba siempre proporcionan al menos las siguientes verificaciones básicas:

  • Una verificación "real", descrita como una verificación "aceptable", comprueba que una condición sea verdadera y coincide con la forma en que podrías escribir una if que verifique si algo es exitoso o correcto. Suele proporcionarse como assert(...) o assert.ok(...), y toma un solo valor más un comentario opcional.

  • Una verificación de igualdad, como en el ejemplo de una prueba matemática, en la que esperas que el valor que se muestra o el estado de un objeto sea igual a un buen valor conocido. Se usan para igualdad primitiva (como números y cadenas) o igualdad referencial (son el mismo objeto). De forma interna, estas son solo una verificación de confianza con una comparación == o ===.

    • JavaScript distingue entre la igualdad flexible (==) y estricta (===). La mayoría de las bibliotecas de prueba te proporcionan los métodos assert.equal y assert.strictEqual, respectivamente.
  • Verificaciones de igualdad profundas, que extienden las comprobaciones de igualdad para incluir la verificación del contenido de objetos, arrays y otros tipos de datos más complejos, así como la lógica interna para desviar objetos y compararlos. Estos son importantes porque JavaScript no tiene una forma integrada de comparar el contenido de dos objetos o arreglos. Por ejemplo, [1,2,3] == [1,2,3] siempre es falso. Los frameworks de prueba suelen incluir asistentes deepEqual o deepStrictEqual.

Los asistentes de aserción que comparan dos valores (en lugar de solo una verificación de "verdad") suelen tomar dos o tres argumentos:

  • El valor real, tal como se genera a partir del código que se está probando o que describe el estado que se va a validar.
  • El valor esperado, generalmente codificado (por ejemplo, un número literal o una cadena).
  • Un comentario opcional que describe lo que se espera o lo que podría haber fallado, que se incluirá si falla esta línea.

También es una práctica bastante común combinar aserciones para construir una variedad de verificaciones, ya que no es común que una persona pueda confirmar correctamente el estado del sistema por sí sola. Por ejemplo:

  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')
  });

Vitest usa la biblioteca de aserciones de Chai de forma interna para proporcionar sus asistentes de aserciones, y puede ser útil consultar su referencia para ver qué aserciones y asistentes podrían adaptarse a tu código.

Afirmaciones fluidas y de BDD

Algunos desarrolladores prefieren un estilo de aserción que puede llamarse desarrollo basado en el comportamiento (BDD) o aserciones de estilo Fluent. También se los llama auxiliares "esperados", porque el punto de entrada para verificar las expectativas es un método llamado expect().

Se espera que los asistentes se comporten de la misma manera que las aserciones escritas como llamadas de método simples, como assert.ok o assert.strictDeepEquals, pero a algunos desarrolladores les resultan más fáciles de leer. Una aserción de BDD puede ser de la siguiente manera:

// 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');

Este estilo de aserciones funciona debido a una técnica llamada encadenamiento de métodos, en la que el objeto que muestra expect se puede encadenar de forma continua con otras llamadas de método. Algunas partes de la llamada, incluidas to.be y that.does en el ejemplo anterior, no tienen ninguna función y solo se incluyen para facilitar la lectura de la llamada y generar un comentario automatizado si la prueba falló. (En particular, expect normalmente no admite comentarios opcionales porque el encadenamiento debería describir la falla con claridad).

Muchos frameworks de prueba admiten tanto las aserciones Fluent/BDD como las regulares. Vitest, por ejemplo, exporta ambos enfoques de Clang y tiene su propio enfoque un poco más conciso para el BDD. Jest, por otro lado, solo incluye un método espera de forma predeterminada.

Cómo agrupar pruebas entre archivos

Cuando se escriben pruebas, ya tendemos a proporcionar agrupaciones implícitas; en lugar de que todas las pruebas estén en un solo archivo, es común escribir pruebas en varios archivos. De hecho, los ejecutores de pruebas solo suelen saber que un archivo es de prueba debido a un filtro predefinido o una expresión regular. Vitest, por ejemplo, incluye todos los archivos del proyecto que terminan con una extensión como ".test.jsx" o ".spec.ts" (".test" y ".spec", más una cantidad de extensiones válidas).

Las pruebas de componentes suelen estar ubicadas en un archivo de intercambio de tráfico con el componente que se está probando, como en la siguiente estructura de directorios:

Una lista de archivos en un directorio, incluidos UserList.tsx y UserList.test.tsx.
Un archivo de componentes y un archivo de prueba relacionado

Del mismo modo, las pruebas de unidades suelen colocarse junto al código que se está probando. Las pruebas de extremo a extremo pueden estar en su propio archivo y las pruebas de integración incluso se pueden colocar en sus propias carpetas únicas. Estas estructuras pueden ser útiles cuando los casos de prueba complejos aumentan y requieren sus propios archivos de compatibilidad que no son de prueba, como las bibliotecas de compatibilidad necesarias solo para una prueba.

Cómo agrupar pruebas dentro de los archivos

Como se usó en los ejemplos anteriores, es una práctica común colocar pruebas dentro de una llamada a suite() que agrupe las pruebas que configuraste con test(). Por lo general, los paquetes no realizan pruebas en sí mismos, pero ayudan a proporcionar estructura agrupando objetivos o pruebas relacionadas mediante una llamada al método aprobado. Para test(), el método aprobado describe las acciones de la prueba en sí.

Al igual que con las aserciones, hay una equivalencia bastante estándar en Fluent/BDD a las pruebas de agrupación. En el siguiente código, se comparan algunos ejemplos típicos:

// 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);
  });
})

En la mayoría de los frameworks, suite y describe se comportan de manera similar, al igual que test y it, en comparación con las mayores diferencias entre el uso de expect y assert para escribir aserciones.

Otras herramientas tienen enfoques ligeramente diferentes para organizar los paquetes y las pruebas. Por ejemplo, el ejecutor de pruebas integrado de Node.js admite llamadas de anidación a test() para crear implícitamente una jerarquía de pruebas. Sin embargo, Vitest solo permite este tipo de anidamiento con suite() y no ejecutará un test() definido dentro de otro test().

Al igual que con las aserciones, recuerda que la combinación exacta de métodos de agrupación que proporciona tu pila tecnológica no es tan importante. En este curso, se abordarán en resumen, pero tendrás que descubrir cómo se aplican a las herramientas que elijas.

Métodos del ciclo de vida

Una razón para agrupar tus pruebas, incluso implícitamente en el nivel superior de un archivo, es proporcionar métodos de configuración y desmontaje que se ejecuten para cada prueba o una vez para un grupo de pruebas. La mayoría de los frameworks proporcionan cuatro métodos:

Para cada `test()` o `it()` Una vez para el paquete
Antes de que se ejecute la prueba antes de cada uno()` antes de todos()`
Después de la ejecución de la prueba después de cada uno `afterAll()`

Por ejemplo, tal vez quieras completar previamente una base de datos de usuarios virtuales antes de cada prueba y borrarla después:

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

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

Esto puede ser útil para simplificar tus pruebas. Puedes compartir un código común de configuración y desmontaje, en lugar de duplicarlo en cada prueba. Además, si el código de configuración y desmontaje arroja un error, esto puede indicar problemas estructurales que no implican que las pruebas fallen.

Consejo general

Estos son algunos consejos que debes recordar cuando pienses en estas primitivas.

Las primitivas son una guía

Recuerda que las herramientas y primitivas que se mencionan aquí y en las próximas páginas no coincidirán exactamente con Vitest, Jest, Mocha, Web Test Runner o cualquier otro framework específico. Si bien usamos Vitest como guía general, asegúrate de asignarlos al framework de tu elección.

Combina aserciones según sea necesario

En esencia, las pruebas son código que puede arrojar errores. Cada ejecutor proporcionará una primitiva, probable test(), para describir casos de prueba distintos.

Sin embargo, si ese ejecutor también proporciona assert(), expect() y asistentes de aserción, recuerda que esta parte es más práctica y puedes omitirla si es necesario. Puedes ejecutar cualquier código que pueda arrojar un error, incluidas otras bibliotecas de aserciones o una declaración if antigua.

La configuración del IDE puede ser útil

Asegúrate de que tu IDE, como VSCode, tenga acceso al autocompletado y a la documentación sobre las herramientas de prueba que elegiste para aumentar tu productividad. Por ejemplo, hay más de 100 métodos en assert en la biblioteca de aserciones de Clang, y puede ser conveniente que la documentación del correcto aparezca intercalada.

Esto puede ser especialmente importante para algunos frameworks de prueba que propagan el espacio de nombres global con sus métodos de prueba. Esta es una diferencia sutil, pero a menudo es posible usar bibliotecas de prueba sin importarlas si se agregan automáticamente al espacio de nombres global:

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

Te recomendamos que importes los asistentes incluso si se admiten automáticamente, ya que eso le brinda a tu IDE una forma clara de buscar estos métodos. (Es posible que hayas experimentado este problema cuando compilaste React, ya que algunas bases de código tienen un React global mágico, pero otras no lo tienen, y requieren que se importe en todos los archivos con React).

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