Cuando se trata de desarrollar software de calidad, una de las herramientas más poderosas que tenemos a nuestro alcance es el Unit Testing.
En este artículo, exploraremos de forma sencilla y accesible todos los aspectos clave sobre los unit tests. Descubrirás sus beneficios, cómo se estructuran, técnicas para nombrarlos adecuadamente, qué son los mocks y el Code Coverage, además de las mejores prácticas para escribir unit tests efectivos.
Antes de iniciar quiero que darte un ejemplo de que ocurriría en un proyecto sin pruebas unitarias.
Imagina que estás desarrollando una nueva aplicación web y has llegado a la fase de pruebas en donde esperas cruzar los dedos para no encontrar errores. Pero, sorpresa ⚠️ siempre aparecen problemas de último minuto, como usuarios que pueden registrarse sin proporcionar una dirección de correo electrónico válida.
Aquí es donde entran en juego las pruebas unitarias, un salvavidas para los desarrolladores. En lugar de esperar hasta el final, las pruebas unitarias se hacen a lo largo del proceso de desarrollo. Básicamente, se prueban pedacitos de código, como funciones o métodos, para asegurarse de que funcionen correctamente.
Volviendo a nuestro ejemplo del registro de usuarios, con pruebas unitarias, podríamos haber detectado el error antes. Hubiéramos creado una prueba que verifica si la dirección de correo electrónico es válida o no. Si la prueba falla, sabemos que hay un problema en esa parte del código y podemos arreglarlo de inmediato.
¡No más sorpresas de último minuto!
Las pruebas unitarias no solo nos ayudan a encontrar errores antes, sino que también facilitan la identificación de la causa raíz. De esta forma, se reducen la cantidad de errores que llegan al producto final, ya que en ocasiones estos errores pueden escaparse incluso en la fase de pruebas.
La Pirámide de Testing
Es importante conocer la pirámide de Testing, ya que en base a esta podemos determinar factores como la rapidez de ejecución, costo, volumen y confiabilidad. Esta pirámide se compone principalmente de las pruebas de interfaz de usuario, pruebas de integración y pruebas unitarias.
- Unit Test:
- Prueba la unidad de la aplicación sin dependencias externas.
- Son rápidas pero no dan tanta confiabilidad.
- Integration Test:
- Prueba la aplicación con dependencias externas.
- Son lentas pero dan más confiabilidad.
- End to end Test:
- Prueba toda la aplicación desde la interfaz de usuario.
- Son muy lentas pero dan muchísima confiabilidad.
Como puedes ver las pruebas unitarias son la base de la pirámide de pruebas y al ser componentes individuales aislados, son ejecutadas rápidamente. En caso de que se encuentren errores en este tipo de pruebas, la corrección del mismo es menos costoso para el proyecto ya que sería detectado en la fase de desarrollo y no en un ambiente productivo o de pruebas.
Con dependencias externas nos referimos a base de datos, web services, APIs, cola de mensajes, entre otros.
¿Qué es un Unit Test?
Un Unit Test, o prueba unitaria, es una técnica de prueba en el desarrollo de software que se enfoca en probar unidades individuales de código, como funciones, métodos o clases, de manera aislada. El objetivo principal es verificar que cada unidad de código funcione correctamente por sí misma, independientemente de su integración con el resto del sistema.
Básicamente es un porción de código para probar de porción de código.
Existen Frameworks o librerías para hacer Unit Testing en diferentes lenguajes de programación:
- XUnit y NUnit para .NET
- JUnit para Java
- Jest y Cypress para Javascript
- Unittest para Python
Ten en cuenta que no todo el código debe ser probado. Algunos ejemplos de que debe probarse:
- Métodos públicos.
- Funciones de tipo query o comandos.
- Cada ruta de ejecución que puede tomar la función.
Por otro lado, no debería ser probado:
- Métodos privados, ya que estos se prueban implícitamente dentro de los públicos.
- Features del lenguaje.
- Código de terceros como librerías.
Beneficios del Unit Testing
- Garantizar que el código haga lo que se supone que debe hacer.
- Generar código robusto.
- Mejorar la calidad de código productivo.
- Permiten hacer Test Driven Development
- Detección temprana de errores
- Facilita el mantenimiento de código
- Fomenta un diseño desacoplado
Estructura de un Unit Test
Un Unit Test generalmente sigue una estructura sencilla pero poderosa. Tiene tres pasos básicos, el famoso AAA: Arrange (Preparación), Act (Acción) y Assert (Verificación).
- Arrange: Inicializamos variables u objetos necesarios para la prueba.
- Act: Invocamos o llamamos el método que queremos probar.
- Assert: Verificar que el valor obtenido sea o no valor esperado.
Para este ejemplo, usaremos el popular de programación FizzBuzz. De forma resumida, si un número es divisible por 3 imprime Fizz, si es divisible por 5 imprime Buzz y si lo es por 3 y 5 a la vez, imprime FizzBuzz, de lo contrario imprime el número.
En estos ejemplos, trato de usar lo menos posible las herramientas que ofrece xUnit para que se pueda aplicar a cualquier tecnología.
Creemos la prueba unitaria para validar la correcta implementación de nuestro método siguiendo la estrategia AAA.
[TestFixture]
public class Tests
{
[Test]
public void FizzBuzzTest()
{
// Arrange
var resultadoEsperado = "FizzBuzz";
var numeroEntrada = 15;
var serviceFizzBuzz = new ServiceFizzBuzz();
// Act
var resultadoObtenido = serviceFizzBuzz.Calculate(number);
// Assert
Assert.That(resultadoObtenido, Is.EqualTo(resultadoEsperado));
}
[Test]
public void FizzTest()
{
// Arrange
var resultadoEsperado = "Fizz";
var numeroEntrada = 3;
var serviceFizzBuzz = new ServiceFizzBuzz();
// Act
var resultadoObtenido = serviceFizzBuzz.Calculate(number);
// Assert
Assert.That(resultadoObtenido, Is.EqualTo(resultadoEsperado));
}
[Test]
public void BuzzTest()
{
// Arrange
var resultadoEsperado = "Buzz";
var numeroEntrada = 5;
var serviceFizzBuzz = new ServiceFizzBuzz();
// Act
var resultadoObtenido = serviceFizzBuzz.Calculate(number);
// Assert
Assert.That(resultadoObtenido, Is.EqualTo(resultadoEsperado));
}
[Test]
public void NumberTest()
{
// Arrange
var resultadoEsperado = "7";
var numeroEntrada = 7;
var serviceFizzBuzz = new ServiceFizzBuzz();
// Act
var resultadoObtenido = serviceFizzBuzz.Calculate(numeroEntrada);
// Assert
Assert.That(resultadoObtenido, Is.EqualTo(resultadoEsperado));
}
}
En ocasiones puede haber más de un paso en una misma línea de código.
Técnicas de Naming
El arte de nombrar nuestras pruebas es como elegir nombres para nuestras mascotas. En el caso de Unit Testing, queremos que los tests sean claros y descriptivos. Sin embargo, esto depende de la convención que elija el equipo de Desarrollo en el proyecto.
Mi favorito cumple con la siguiente nomenclatura:
- Para el caso de un proyecto: [Nombre de proyecto].UnitTests. Por ejemplo, si el proyecto se llama ApiFly, el proyecto de Unit Test se llamará ApiFly.UnitTests.
- Para el caso de las clases: [Nombre de clase]Tests. Por ejemplo, si la clase se llama ServiceTour, la clase de Unit Testing se llamará ServiceTourTests.
- Para el caso de los métodos: [Nombre del método]_[Escenario]_[Valor esperado]. Por ejemplo, si el método se llama ValidateFlightDate, el método de Unit Testing se llamará ValidateFlightDate_DateBeforeToday_ReturnFalse.
[TestFixture]
public class ServiceFizzBuzzTests
{
[Test]
public void Calculate_WhenIsMultipleThreeAndFive_ReturnFizzBuzz()
{
// Arrange
var resultadoEsperado = "FizzBuzz";
var numeroEntrada = 15;
var serviceFizzBuzz = new ServiceFizzBuzz();
// Act
var resultadoObtenido = serviceFizzBuzz.Calculate(number);
// Assert
Assert.That(resultadoObtenido, Is.EqualTo(resultadoEsperado));
}
[Test]
public void Calculate_WhenIsMultipleThree_ReturnFizz()
{
// Arrange
var resultadoEsperado = "Fizz";
var numeroEntrada = 3;
var serviceFizzBuzz = new ServiceFizzBuzz();
// Act
var resultadoObtenido = serviceFizzBuzz.Calculate(number);
// Assert
Assert.That(resultadoObtenido, Is.EqualTo(resultadoEsperado));
}
[Test]
public void Calculate_WhenIsMultipleFive_ReturnBuzz()
{
// Arrange
var resultadoEsperado = "Buzz";
var numeroEntrada = 5;
var serviceFizzBuzz = new ServiceFizzBuzz();
// Act
var resultadoObtenido = serviceFizzBuzz.Calculate(number);
// Assert
Assert.That(resultadoObtenido, Is.EqualTo(resultadoEsperado));
}
[Test]
public void Calculate_WhenIsMultipleThreeAndFive_ReturnFizzBuzz()
{
// Arrange
var resultadoEsperado = "7";
var numeroEntrada = 7;
var serviceFizzBuzz = new ServiceFizzBuzz();
// Act
var resultadoObtenido = serviceFizzBuzz.Calculate(numeroEntrada);
// Assert
Assert.That(resultadoObtenido, Is.EqualTo(resultadoEsperado));
}
}
Otros desarrolladores prefieren la siguiente nomenclatura:
- Para el caso de un proyecto: Agregar ".Tests". Por ejemplo, si el proyecto se llama ECommerce, el proyecto Unit Testing se llamaría ECommerce.Tests.
- Para el caso de las clases: Agregar "Should". Por ejemplo, si la clase se llama CatalogService, la clase de Unit Testing se llamaria CatalogServiceShould.
- Para el caso de los métodos: Se nombre con lo que debe retornar bajo determinado escenario. Por ejemplo, si el método nos devuelve true si existe algún producto en Stock, sería llamado Return_True_Exists_Stock().
Existen muchas otras más, aunque estas son las más comunes.
Si estas en un equipo, deberás mantener la convención que elijan o que ya este establecida para mantener un estándar.
¿Qué es un mock?
Un mock es como un actor de cine que interpreta a alguien más en una película. En el Unit Testing, es un objeto que simula ser una dependencia externa de nuestra unidad de código. Por ejemplo, si nuestra función depende de una API, en lugar de llamar a la API real en las pruebas, usamos un mock que actúa como si fuera la API. Los mocks nos permiten remover dependencias externas y hacer nuestras pruebas más controladas y predecibles.
Algunas librerías para hacer mocks en nuestras pruebas unitarias:
- Moq y NSubstitute para .NET
- Mockito y EasyMock para Java
- Jest para Javascript
- Unittest.mock para Python
¿Qué es el Code Coverage?
El Code Coverage, o cobertura de código, es como una radiografía de nuestro código. Nos muestra cuántas partes han sido cubiertas por nuestras pruebas unitarias.
Por lo tanto, el Code Coverage nos ayuda a evaluar qué tan bien hemos probado nuestro código, identificando las partes que aún no hemos tocado. Una alta cobertura de código nos da la confianza de que estamos cubriendo la mayoría de los rincones de nuestro software.
Es poco probable que a medida que el software crezca se tenga una cobertura de 100%. Pero siempre hay que tratar de mantener un alto porcentaje.
Existen muchas herramientas para medir el Code Coverage, algunas integradas al IDE y otras aisladas del editor como Sonarqube que aparte de darte otras métricas de calidad, también te indica la cobertura de código.
Recomendaciones para tus pruebas unitarias
- Crea pruebas unitarias cortas y legibles.
- Mantén la independencia en tus pruebas. Para ello, evita utilizar recursos externos y en su defecto usa mocks.
- Si tienes que crear muchos test para un único método, revisa detenidamente el método porque probablemente sea mejor una refactorización.
- Utiliza alguna técnica de Naming en tus pruebas y mantén el mismo durante todo el proyecto.
- Evita duplicación de código en tus pruebas, revisa si puedes crear métodos privados para la reutilización que no generen efectos secundarios.
- Evalúa la cobertura de código en tus proyectos. No es necesario tener un 100% pero de 70% para arriba es aceptable.
- Testear sólo las interfaces públicas.
- No debes crear pruebas tan específicas ni tan generales.
- Crear pruebas basada en el número de caminos que puede tomar tu código. Para esto ten en cuenta las condicionales dentro del método que quieres probar.
Espero que te haya servido esta guía sobre Unit Testing y que empieces aplicarlo en tus proyectos 🚀.
¡Happy coding!💻
Top comments (0)