Escribir pruebas

Las pruebas pueden escribirse de diversas maneras. Esta sección intenta ofrecer un estándar y una justificación detrás de la forma en que escribimos pruebas.

Las pruebas DEBEN escribirse usando el describe y el it métodos provistos por jest

Describing and building context

El describe el método DEBERÍA utilizarse para describir, siempre que sea posible, el contexto en el que se ejecutará el fragmento de código a probar y DEBERÍA construir ese contexto dentro de su propio ámbito.

describe('when the flag is red', () => {
	...
})

describe('when the flag is blue', () => {
	...
})

Para describir el contexto, DEBERÍAMOS usar una de las siguientes palabras: when para describes de nivel superior y having o y para indicar que hay un contexto que depende de otro contexto definido anteriormente. La razón detrás de esto es tener contextos auto-descriptivos que puedan entenderse y comprobarse fácilmente combinando las frases describe. Dado que la idea es concatenar las descripciones de los contextos, describes DEBEN escribirse en minúsculas.

describe('when the flag is red', () => {
	...
	describe('and the wind is strong', () => {
     // El contexto aquí está descrito por la conjunción de los describes:
     // "when the red flag is red and the wind is strong"
     ...
  })
})

Múltiples describes pueden estar anidados al mismo nivel y en niveles inferiores. Los describes en el mismo nivel denotan contextos diferentes y los describes en niveles inferiores (describes anidados) denotan un contexto más específico.

Como se mencionó antes, cada describe DEBERÍA ir acompañado de un método que construya el contexto o un método que destruya o cambie el contexto. Los métodos que DEBEN usarse son el beforeEach y el afterEach.

circle-exclamation

Contextos creados con el beforeEach método se irán definiendo cada vez más a medida que profundizamos en el árbol de describe. Cada beforeEach especificará el contexto más de acuerdo con lo que su texto indica.

Ámbito de variables

El variables definidas en los contextos DEBERÍAN declararse de acuerdo con el alcance en el que se van a usar. Es decir, si una variable se usa solo para un contexto específico en un nivel anidado de describes, la variable DEBERÍA definirse únicamente en ese contexto específico. La razón detrás de esto es mantener las variables cerca del código en el que se usan, facilitando la comprensión del código.

Variables de tipos primitivos, que no serán cambiadas en el contexto en el que se definen o en contextos posteriores DEBERÍAN definirse como const una vez, sobre la beforeEach definición en el ámbito.

Variables de tipos primitivos que serán cambiados en el contexto en el que se definen o en contextos posteriores DEBEN definirse como let, según obliga la naturaleza de Typescript.

Variables de tipos no primitivos (objetos, arrays, etc.) DEBEN definirse como let y su valor DEBE establecerse en un beforeEach. La razón detrás de esto es que los objetos en JS son mutables, lo que significa que cualquier ejecución de código que use ese objeto está sujeta a cambios y puede afectar otras pruebas de forma involuntaria.

Las variables DEBERÍAN estar correctamente tipadas siempre que sea posible. Los tipos repetidos DEBERÍAN abstraerse en un tipo.

Describiendo expectativas y ejecutando código

El it el método DEBE colocarse siempre dentro del ámbito de un describe y DEBE usarse para describir qué esperamos del código que vamos a probar y DEBERÍA contener, si es posible, solo una aserción. Pueden existir múltiples aserciones por it si hay un problema de rendimiento, ya que las pruebas se ejecutarán una vez por it (debido al beforeEach), o si lo esperado puede describirse con claridad en la descripción de la expectativa.

La razón detrás de esta estructura es proporcionar claridad al revisor de las pruebas y a los desarrolladores que mantendrán y harán cambios en el código, ya que cada contexto y expectativa está claramente enumerado, lo que facilita comprender qué se está probando, cómo se prueba y qué queda por probar.

Siguiendo los describe descripciones hasta los its los desarrolladores pueden entender fácilmente qué se está probando concatenando las frases.

Escribir expectativas claras

La descripción de las expectativas escritas en los its DEBE ser lo más descriptiva posible sobre lo que se espera de la ejecución de las pruebas. Los desarrolladores NO DEBEN usar formulaciones abstractas o generales para definir expectativas, ya que quitan claridad sobre lo que se espera del código.

triangle-exclamation

Debe tenerse en cuenta que al especificar lo que se espera en los its o cuál será el contexto en los describes los desarrolladores PUEDEN usar nombres de funciones o mensajes de error exactos si se requiere para entender mejor la intención, pero DEBERÍAN usar una representación textual cuando sea posible para facilitar el mantenimiento de las pruebas.

Qué probar

Elegir qué probar puede variar según el código que se esté ejecutando. Diferentes factores, como el rendimiento o un gran dominio de entradas, pueden cambiar definitivamente lo que se debe o no probar. Aquí expondremos un único conjunto de casos que el desarrollador DEBERÍA probar si es posible.

La función run aquí tiene un par de flujos de ejecución diferentes. Las funciones, o secciones de código, NO DEBERÍAN probarse basándose únicamente en los diferentes flujos de ejecución posibles, sino en lo que se espera de la función, ya que la función podría no estar haciendo aquello para lo que fue escrita y las pruebas podrían quedar fuertemente acopladas al código y no detectar distintos problemas. Esto implica que el desarrollador DEBERÍA siempre probar todos los posibles caminos de ejecución y DEBERÍA probar otros caminos posibles también de acuerdo con la semántica de la función.

Para este caso específico, el desarrollador DEBERÍA probar al menos los siguientes casos:

  1. Llamar a la función run con una cantidad de kilómetros mayor que 1000

  2. Llamar a la función run con una cantidad de kilómetros igual a 10

  3. Llamar a la función run con una cantidad de kilómetros mayor que 10 pero menor que 1000

Para el primer caso, el desarrollador necesita probar que la función run lanza una excepción. Las excepciones DEBEN comprobarse por su mensaje de error, ya que cualquier otra excepción también podría lanzarse, invalidando el propósito de la prueba. Si la excepción es una excepción personalizada, el desarrollador PUEDE comprobar la instancia de la excepción.

Para el segundo caso, el desarrollador necesita probar que la función devolvió el mismo número de kilómetros que se le proporcionó.

Para el tercer caso, el desarrollador necesita probar que la función devolvió lo que una función externa, beLazy devolvió y, como doSomething (también ejecutada en el método) no puede comprobarse mediante el valor de retorno de la función, el desarrollador DEBERÍA probar que fue llamada con los argumentos correctos.

Cuándo hacer mock

El uso de mocks dependerá principalmente del tipo de pruebas que esté escribiendo el desarrollador.

  • Las pruebas unitarias DEBERÍAN tener todas las funciones externas mockeadas, con la excepción de funciones que realizan tareas simples, como formatear cadenas, etc.

  • Las pruebas de API SOLAMENTE DEBERÍAN tener mocks para la comunicación con servicios externos, es decir, bases de datos o APIs diferentes. Si una de las operaciones realizadas en la prueba afecta el rendimiento del conjunto de pruebas, se podría implementar un mock para mitigar este problema.

Todos los mocks DEBEN realizarse usando las herramientas proporcionadas por Jest, con la excepción de los redux-saga-test-plan mocks.

Mocks y funciones utilitarias

  • Cualquier mock de Jest DEBERÍA mockearse usando su once método cuando sea posible, es decir, mockReturnValueOnce o mockResolvedValueOnce se recomienda en contra mockReturnValue o mockResolvedValue. La razón detrás de esto es prevenir que mocks no deseados se ejecuten, cambiando la ejecución de nuestras pruebas.

  • Jest ofrece un conjunto de Types que puedes usar para diferentes tipos de mocks. Por ejemplo, al mockear un objeto DEBERÍAS usar jest.Mocked<typeof someObject>, para clases DEBERÍAS usar jest.MockedClass<typeof SomeClass>, y para funciones, jest.MockedFunction<typeof someFunction>.

  • Un afterEach DEBERÍA escribirse para ejecutarse después de cada prueba con un jest.resetAllMocks para limpiar cualquier implementación mockeada de módulos mockeados globalmente que pueda filtrarse a otras pruebas. Puedes omitir este reset si estás usando mockReturnValueOnce o mockResolvedValueOnce porque se hace automáticamente por ti.

  • Un directorio llamado mocks PUEDE crearse para almacenar mocks, que deberían colocarse en el directorio directorio de prueba o spec . Los mocks grandes DEBEN almacenarse en archivos diferentes a las pruebas para evitar tener archivos de pruebas extensos. Los mocks pequeños, si no son muchos, DEBERÍAN mantenerse en las pruebas.

    • Los archivos mock DEBERÍAN nombrarse como la entidad que están mockeando, así que por ejemplo, si estamos mockeando un profile, el mock debería colocarse bajo el /test/mocks/profile.ts archivo.

    • En caso de que se requieran varios mocks grandes, DEBERÍAN colocarse en archivos diferentes dentro de un directorio llamado como la entidad a mockear, así que por ejemplo, si tenemos dos mocks de profile, los almacenaremos como: /test/mocks/profile/profile-with-wearables.ts y /test/mocks/profile/profile-without-wearables.ts. Para acceder a ellos fácilmente, DEBERÍAS crear un index.ts archivo dentro del /test/mocks/profile/ directorio y exportarlos.

    • Para evitar la mutación de objetos mockeados, los mocks DEBEN exportarse como funciones que los devuelvan.

Última actualización