Cómo estructurar nuestras pruebas unitarias?

July 23, 2019

...

Uno de los grandes desafíos que tiene todo desarrollador, tarde o temprano es empezar a escribir tests automatizados a nuestras aplicaciones. De donde yo vengo son muy pocas las empresas que toman como practica el test driven development (desarrollo orientado por pruebas), y esta practica, si se realiza de forma correcta, puede ayudar a reducir los costos en el largo plazo. Los desarrolladores también se ven beneficiados por esta práctica ya que el código que será escrito es mucho más fácil de entender debido a las practicas que debes empezar a tomar para poder escribir pruebas de manera eficiente. Acá veremos un pequeño ejemplo de cómo escribir pruebas a un backend escrito con javascript.

Para poder tener un ejemplo claro sin mucha complejidad vamos a construir una prueba a una función que se le pasa como argumento a un endpoint rest hecho en Express.js

import express from 'express'
import db from './db'

express.post('/users', async (req, res) => {
  try {
    const { id } = await db.save(req.body)
    res.status(201).send({ id })
  } catch (e) {
    res.status(500).send(e)
  }
})

Este ejemplo es quizás lo que más he visto cuando en bases de código. El problema que presenta es que es difícil poder escribir test a una función que importa efectos ya que puede estar realizando conexiones a la base de datos de manera silenciosa como es el caso de la librería mongoose.

En el caso de que esta no despache ningún efecto, de igual manera estamos haciendo un llamado directo al método save de nuestra abstracción de la base de datos. Y si no tenemos configurada una base de datos de manera correcta, este código puede fallar, y escribirle pruebas a este código donde no sabremos si este pasará o fallará es un tremendo dolor de cabeza para cualquier desarrollador.

No todo esta perdido, vamos a ver un patrón (uno de mis favoritos) que nos permitirá escribirle pruebas a todo nuestro javascript de manera muy sencilla.

Presentando: Inyección de dependencias

El patrón de inyección de dependencias pertenece a la familia de inversión de control, donde el objetivo principal es que los efectos introducidos por nuestro código sean manejados de manera determinística (conociendo el input, predecir el output). Sin embargo esto solo lo podremos saber si conocemos el resultado de nuestro método save que, en este caso, es generado por nuestro motor de base de datos, y que es lo que hacen motores como MySQL, MongoDB entre otros.

Manipulando la salida de la base de datos

Para poder saber la salida que tendrá nuestra base de datos debemos poder simular el comportamiento de la base de datos, esto se conoce como mocking, y siempre que nosotros tengamos que construir un Mock, debemos tener especial cuidado ya que podríamos simular un comportamiento erróneo. Pero eso lo veremos en otra entrada de blog ya que merece su propio apartado.

Por ahora vamos a cambiar nuestro código para que sea fácil escribirle tests en varios pasos:

import express from 'express'
import db from './db'

express.post('/users', createUser({ db }))

const createUser = ({ db }) => async (req, res) => {
 try {
   const { id } = await db.save(req.body)
   res.status(201).send({ id })
 } catch (e) {
   res.status(500).send(e)
 }
}

Calma! Vamos a ver que ocurrió:

  1. Movimos la función y le asignamos un nombre, esta dejó de ser una función anónima y pasó a llamarse createUser.
  2. Aplicando el patrón de currying le entregamos a nuestra función el servicio de base de datos.
  3. Utilizando el estilo point free nos deshicimos de los argumentos de req y res al momento de llamar a esta función.

Currying: este patrón permite entregarle argumentos a una función bajo demanda.

Point free: es un estilo de programación donde los argumentos de una función se omiten, de esta manera se elimina boilerplate innecesario en nuestra aplicación.

Al utilizar los patrones mencionados anteriormente conseguimos dos cosas: hacer que nuestro código sea fácil de escribirle tests y también la implementación es más declarativa, esto quiere decir que no vemos cómo se va a crear el usuario, pero si vemos que se crea a un usuario. Por lo que podemos preocuparnos de esta manera en lo que hace nuestro código en lugar de como lo hace.

Luego de esto podemos mover nuestra función de create user a otro archivo. Por lo que nuestra ruta de crear usuario quedaría más despejada:

import express from 'express'
import db from './db'
import createUser from './createUser'

express.post('/users', createUser({ db }))

Mucho mejor. Ahora este archivo lo utilizaremos solo para importar nuestras dependencias y también funciones que ejecutaremos. Para algunos estos son controladores, para otros handlers, pero nosotros los llamaremos “funciones” para darles un uso bastante más generalizado. Al final de este artículo veremos esto más en detalle.

Luego de haber movido nuestra función a otro archivo, cuando la importemos para crearle tests, ya no creará efectos innecesarios, además de que podremos pasarle una base de datos simulada por nosotros! Veamos como quedó finalmente nuestro archivo createUser:

export const createUser = ({ db }) => async (req, res) => {
 try {
   const { id } = await db.save(req.body)
   res.status(201).send({ id })
 } catch (e) {
   res.status(500).send(e)
 }
}

Pero que elegancia. Ningún import en nuestro archivo, todo es pasado como argumento a esta función por lo que sabemos exactamente qué está utilizando y que no.

Luego de haber movido nuestra función a otro archivo y asegurarnos de que nuestro código sigue funcionando, ya podemos empezar a escribirle pruebas a esta función. Como este artículo no trata sobre testing per se si no de como estructurar nuestro código para que sea fácil de escribirle pruebas, vamos a escribir el test con Jest pero sin profundizar mucho:

import { createUser } from './createUser'

describe('createUser', () => {
  it('camino feliz' async () => {
    const db = {
      save: jest.fn().mockResolvedValue({ id: 'mi id' })
    }
    const req = { body: 'body del request' }
    const res = {
      status: jest.fn().mockReturnValue(res),
      send: jest.fn()
    }
    await createUser({ db })(req, res)
    expect(db.mock.calls).toEqual([
      ['body del request'],
    ])
    expect(res.status.mock.calls).toEqual(
      [201],
    )
    expect(res.send.mock.calls).toEqual(
      [{ id: 'mi id' }],
    )
  })
})

Y listo, fácil de escribirle pruebas! Hasta se ve más ordenada la ruta.


Suscríbete

Suscríbete a la lista para más cursos, posts y videos tutoriales. Prometo no enviarte más de un correo semanal 🙏

Creado por Nicolás Schürmann ingeniero e instructor de software. Cuando no está programando, esta frente a una cámara dictando cursos, creyéndose youtuber o apoyando a sus alumnos. Puedes seguirlo en twitter o también suscribirte a su canal de youtube. Considera comprar sus cursos por este medio y así apoyas al instructor.