¿Cómo el uso de pruebas unitarias puede prevenir desastres en sistemas informáticos?

Mauricio Morales

Software Developer

julio 28, 2020

Lectura de 8 minutos

Cuando se empieza el camino profesional como desarrollador de software, es común pensar en lo divertido que resultará escribir código para solventar cualquier tipo de problema o necesidad que nuestros clientes requieran, en este afán nuestras prioridades suelen ser aprender los diferentes lenguajes de programación, frameworks, patrones de diseño, buenas prácticas y demás conceptos, técnicas y herramientas que ayudan a agilizar y optimizar la escritura de código y su funcionamiento.

Sin embargo, un área que es bastante olvidada, sobre todo en startups y empresas pequeñas dedicadas al desarrollo de software, es precisamente la adopción de herramientas que permitan escribir, ejecutar y medir pruebas unitarias y de integración, muchas personas piensan que esto resulta en una pérdida de tiempo. En este artículo explicaré por qué ejecutar este tipo de pruebas debe ser parte de la rutina y la disciplina de un desarrollador de software, para ello es importante primero entender ciertos conceptos y por qué no, escribir un par de pruebas.

TDD

Cuando hablamos de pruebas unitarias es preciso hablar de TDD, por sus siglas en inglés “Test Driven Development”. Este es un enfoque que permite a los desarrolladores escribir código basándose en las pruebas que son previamente escritas, con la finalidad de que la escritura del código (que satisfaga la prueba) se desarrolle de forma natural y ágil. TDD tiene el siguiente flujo de trabajo.

Flujo pruebas unitarias

Cómo puedes apreciar en el esquema, TDD es bastante estricto en cuanto al orden de lo que debemos hacer para la escritura de una prueba. En lo personal, no sigo este orden de manera rigurosa, pues considero que cada desarrollador puede adaptar este proceso a su flujo de trabajo.

Por ejemplo en mi día a día escribo mi código de la siguiente manera:

  • Defino el método que voy a implementar en su respectiva interfaz.
  • Escribo la implementación del método de tal forma que devuelva la respuesta que espero, obviando cualquier lógica interna.
  • Escribo la prueba con aserciones super básicas, como que el resultado no sea indefinido.
  • Integro la lógica de lo que hará mi método paso a paso y a la vez voy añadiendo a mi prueba lo que necesito; por ejemplo si mi primer paso es ir a buscar un registro en la base datos, en la prueba haré el respectivo cambio para simular esa respuesta, y a su vez voy ajustando mis aserciones con relación al problema que intento resolver.
  • El paso anterior será un bucle hasta obtener la lógica completa, de igual manera si hubiese condicionales o errores que debo probar, al final tendré que poner pruebas adicionales para satisfacer estos casos.
  • Finalmente, ajusto las aserciones de mis pruebas y realizó un úlitmo análisis en caso de que tenga que hacer pequeñas refactorizaciones en mis pruebas o en el código mismo.

Se pueden escribir pruebas de alta calidad mientras tengamos en mente qué queremos probar y por qué lo hacemos; en un inicio quizás sean conceptos turbios, pero estos se irán esclareciendo a medida que vayas ganando práctica y experiencia.

Pruebas unitarias

Consisten en la validación del fragmento mínimo de código, que puede ser desde un método o función hasta una pequeña clase, esto dependerá mucho del contexto en el cual nos encontremos.

A continuación detallo cómo escribir una prueba unitaria usando el framework Mocha y la librería de aserciones Chai en el entorno de ejecución de Javascript para backend, Node.js.

Inicializamos nuestro proyecto en Node usando el siguiente comando desde la terminal ubicados en el directorio que hayamos seleccionado.

npm init

Este comando nos preguntará varias cosas respecto a nuestro proyecto, para este ejemplo dejaremos los valores por defecto. Lo importante es que como resultado de la ejecución de este comando obtengamos el archivo package.json dentro de la raíz de nuestro directorio.

{
  "name": "unittesting",
  "version": "1.0.0",
  "description": "simple examples about unit testing with mocha on node.js",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Instalamos Mocha y Chai como dependencias de desarrollo.

$ npm install --save-dev mocha chai

Creamos en archivo service.spec.js dentro del directorio src en la raíz del proyecto. Escribimos una prueba para que dada una función que recibe el parámetro greetType, si el valor es 1, el resultado de su ejecución sea un saludo amable con el texto en inglés Hello, how are you sir?” y otra prueba para cualquier otro valor, el resultado será Hi, What's up bro.

const expect = require("chai").expect;
const service = require("./service");

describe("Unit Test Examples", function() {
  it("getMessage should greet kindly", function() {
    const greetType = 1;
    const response = service.greet(greetType);   

    expect(response).to.be.eqls(`Hello, how are you sir?`);
  });

  it("getMessage should greet", function() {
    const greetType = 1;
    const response = service.greet(greetType);   

    expect(response).to.be.eqls(`Hi, What's up bro`);
  });
});

En este fragmento, importamos nuestro archivo service.js que contendrá nuestro método getMessage, usamos describe e it, provistos por Mocha, para encapsular nuestras aserciones y para escribir nuestra prueba como tal, ambos reciben un primer parámetro que es una descripción de lo que contienen, es decir lo que vamos a probar, y una función que se encargará de ejecutar nuestras pruebas.

Ejecutamos nuestras pruebas ejecutando el comando en la terminal.

$ npx mocha ./service.spec.js

Como podíamos esperar el resultado es un error, dado que aún no hemos escrito el código de la función y exportado la misma desde el archivo service.js.

 Unit Test Examples
   1) getMessage should greet kindly
   2) getMessage should greet

  0 passing (20ms)
  2 failing

  1) Unit Test Examples
  getMessage should greet kindly: TypeError: service.greet is not a function at Context.<anonymous> (src\service.spec.js:7:34)

  2) Unit Test Examples
  getMessage should greet: TypeError: service.greet is not a function at Context.<anonymous> (src\service.spec.js:14:34)

Escribe el código de la función que satisfaga la prueba en el archivo service.js dentro del directorio src.

module.exports = {
  greet:  function (greetType) {
    if(greetType === 1)
      return  `Hello, how are you sir?`;
    else
      return  `Hi, What's up bro`;
  }
};

En este fragmento simplemente exportamos la función greet, que recibe como argumento un número y devuelve un mensaje a partir de dicho número.

Volvemos a ejecutar las pruebas.

$ npx mocha ./service.spec.js

Ahora sí obtendremos resultados satisfactorios.

Unit Test Examples
   √ getMessage should greet kindly
   √ getMessage should greet

  2 passing (26ms)

Supongamos que hicimos una refactorización del código, y esta vez el número que saluda amablemente será el 3.

module.exports = {
  greet:  function (greetType) {
    if(greetType === 3)
      return  `Hello, how are you sir?`;
    else
      return  `Hi, What's up bro`;
  }
};

Ejecutamos nuevamente las pruebas.

Unit Test Examples 
    1) getMessage should greet kindly
	    √ getMessage should greet

     1 passing (23ms)
     1 failing

	    1) Unit Test Examples
	    getMessage should greet kindly:

	    AssertionError: expected 'Hi, What\'s up bro' to deeply equal 'Hello, how are you sir?'

	    + expected - actual

	    + -Hi, What's up bro
	    + +Hello, how are you sir?

Y como podíamos imaginar, esta vez una de las pruebas ha fallado debido a los cambios que hemos hecho en nuestra función.

Con ese pequeño ejercicio podemos empezar a comprender lo que ganamos al escribir pruebas unitarias para nuestro código.

Beneficios de realizar pruebas unitarias

  • Agilidad. Hoy en día los sistemas informáticos son cada vez más cambiantes en el tiempo, por lo que estos deben ir adaptándose a los requerimientos que traten de satisfacer. Por ejemplo, un sistema sencillo de procesamiento de imágenes probablemente requiera más formatos procesables. Las pruebas unitarias nos ayudarán a que cualquier cambio que hagamos en nuestro código no afecte a los casos de uso que están funcionando sin errores o bugs.
  • Calidad. Para toda empresa seria que se dedica al desarrollo de software o depende de software desarrollado puertas adentro, es de vital importancia su preocupación en la calidad del código de sus aplicaciones. Las pruebas unitarias sirven como un cedazo que va a permitir filtrar la mayor cantidad de bugs posibles dentro de sus módulos y corregirlos a tiempo, evidentemente no podremos reducir la totalidad de estos incidentes, pero los mitigará en un gran porcentaje.
  • Facilita el análisis y depuración del código. Las pruebas unitarias nos pueden ayudar a hacer debug de nuestro código de forma mucho más sencilla, si tenemos errores en ambiente de producción y no sabemos su causa podemos empezar a ejecutar pruebas en ambiente local y jugar con los parámetros que podrían generar el error, esto nos ahorra tiempo al no tener que lanzar un parche sin saber siquiera si va o no a funcionar.
  • Diseño. Cuando pensamos en las cosas que debemos probar, es un efecto secundario también planear el diseño que va a tener nuestro código. En este caso surgirán preguntas como: ¿en realidad este método debe ir dentro de esta clase? y ¿Puedo desacoplar este método usando una clase estática?. Las respuestas a estas preguntas nos dan un feedback para mejorar la arquitectura y el diseño de nuestro código, lo cual a su vez resulta en una mayor legibilidad y mantenibilidad del mismo.
  • Ahorro de recursos. En toda industria el tiempo y el dinero son factores importantísimos; los anteriores beneficios se resumen en una disminución en el tiempo que los desarrolladores usan para resolver bugs, realizar cambios, adaptaciones o mejoras. Esto, a su vez, les permite ser más productivos y generar mayor valor para la empresa.

Consejo de desarrollador a desarrollador

Cuando empecé mi carrera profesional jamás le di importancia a nada más que a escribir el código que necesitaba para resolver los problemas que las aplicaciones que mis clientes requerían. Muchas veces pasaba horas probando manualmente el código para resolver bugs o para aplicar cambios a mi código. Por ello uno de los retos más grandes que tuve al empezar a trabajar en Kushki fue adaptarme al esquema tan estricto que la empresa mantiene en referencia a las pruebas unitarias, pues cada mínima línea de código debe ser probada.

Así aprendí que, como desarrolladores debemos invertir un gran porcentaje de nuestro tiempo en aprender a escribir pruebas que generen valor para nuestros equipos de trabajo, nuestros proyectos, empleadores y clientes. Investigar nuevas herramientas de testing que nos provean mayor agilidad, calidad, eficiencia, seguridad y mantenibilidad es fundamental. No importa el lenguaje en el cual aprendemos a escribir estas pruebas, pues al momento de cambiar de tecnología, ya tendremos los cimientos suficientes para poder migrar sin mayores dolores de cabeza.

Conclusión

Existen infinidad de tecnologías y herramientas que nos permiten escribir pruebas unitarias, está en cada equipo de trabajo investigar y escoger las que más se acoplen al flujo de trabajo y a los objetivos que se quieren cumplir, así como a los diferentes lenguajes de programación que existen en el mercado de esta industria.

A pesar de que el concepto de prueba unitaria es pequeño y conciso, es un recurso extremadamente poderoso. Como hemos visto no hay ninguna excusa para no escribir pruebas unitarias en nuestros proyectos. Los beneficios son apreciables desde el primer momento en que decidimos aplicarlas, nos ayudan a entender muchísimo mejor las tecnologías que estamos usando y los problemas que estamos resolviendo.

En un caso extremo nuestras pruebas unitarias pueden llegar a salvarnos de un desastre total, solo imagina el aumentar o quitar un par de ceros en transacciones bancarias, y que este error se reproduzca millones de veces antes de ser detectado.

¿Te gustaría mantenerte al tanto de nuestro contenido? Suscríbete a nuestra lista de correos.