La práctica llamada desarrollo guiado por pruebas o por sus siglas en ingles TDD Test Driven Development) es algo que no he encontrado en los proyectos que he visto y participado mas sin embargo considero que todos los proyectos deberían tener prueba automatizadas debido principalmente a dos razones: he visto lo tedioso y susceptible de error que son las pruebas manuales y la facilidad con que los programadores cometemos el mismo error más de una vez en nuestro código.

Nunca pidas permiso para refactorizar. Nunca pidas permiso para escribir pruebas. Haces estas cosas porque SABES que son la mejor manera de avanzar más rápido. Cuando pides permiso, le estás pidiendo a alguien que se haga responsable de tus acciones. Tio Bob Martin

En la practica del TDD se busca codificar inicialmente las pruebas que cubran los requerimiento de proyecto y posteriormente escribir la menor cantidad de código que permita pasar las pruebas implementando el ciclo conocido como Rojo-Verde-Refactorizar o (Red-Green-Refactor). Este ciclo toma su nombre de los colores que usan los ejecutores de pruebas para rojo identificar una prueba fallida, verde una prueba satisfactoria, y refactorización es la acción que lleva a cabo el programador para pasar la prueba y aumentar la calidad del código.

El ciclo Red-Green-Refactor lleva 3 etapas : La primera RED, implica escribir una prueba unitaria está debe fallar ya que aun no contamos con el código escrito y el ejecutar de pruebas la marca en rojo. La segunda etapa se alcanza cuando se ha escrito código suficiente para pasar la prueba unitaria y el ejecutor de pruebas indica un estado verde. El último paso es refactorizar nuestro código para este optimizado y que sea que sea legible se debe asegurar que el estado quede en verde.

En este articulo se da una guía de como crear un proyecto con pruebas unitarias usando C# , x.unit.net y la línea de comando dotnet. Para crear el proyecto se usa la versión preliminar del SDK de .NET Core 3.0 pero funcionará igual para cualquier otra versión.

A pesar de que existen gran cantidad de marcos de pruebas unitarias como NUnit y MSTest preferimos usar xunit.net principalmente porque es el marco de pruebas elegido por los proyectos de código abierto ASP.NET Core y Entitity Framework Core y tengo la idea de en algún momento contribuir con algo de código. He de decir que ahora mismo esa posibilidad la veo lejana pero aun asi tengo la ilusión de poder hacerlo. Los marcos de pruebas unitarias tambíen se pueden usar para crear pruebas de integración ver por ejemplo:

Elegimos hacerlo con la linea de comandos por dos razones porque me gusta entender como funcionan las cosas detrás de cámaras y es necesario saber estas cosa para configurar un Pipeline de entrega continua en Azure Devops o GitLab. Lo que es básicamente un archivo con extensión .yml (ver ejemplo a continuación) donde se especifican las pasos necesarios (instrucciones para la linea de comandos) para construir, probar y desplegar nuestra aplicación. En mi trabajo uso Gitlab Comunnity Edition para el control de código fuente en un servidor en alojado en la red interna. Altamente recomendable.


image: microsoft/dotnet:latest
variables:
  OBJECTS_DIRECTORY: 'obj'
  NUGET_PACKAGES_DIRECTORY: '.nuget'
  SOURCE_CODE_PATH: '*/*/'
stages:
  - build
  - test.
# se omite parte del archivo
before_script:
  - 'dotnet restore --packages $NUGET_PACKAGES_DIRECTORY'
build:
  stage: build
  script:
    - 'dotnet build --no-restore'
tests:
  stage: test
  script:
    - 'dotnet test --no-restore'

Biblioteca de clases con proyecto de pruebas unitarias

Por simplicidad en este ejercicio suponemos que requerimos escribir un método estático para invertir una cadena. Èl método InvierteCadena estará en una clase estática llamada Utilidades Este método debe cumplir con los siguientes requisitos:

  • regresar null si le mandamos una cadena nula
  • regresar string.Empty si le mandamos una cadena vacía.
  • regresar la misma cadena si la longitud de la cadena es una
  • regresar aloh si le mandamos hola
  • regresar aloH si le mandamos Hola

La versión inicial del método sera InvierteCadena es igual a la que utiliza la característica Implementar Interfaz de Visual Studio. Esto es necesario para poder compilar la aplicación.

public static string InvierteCadena(string entrada)
{
  throw new NotImplementedException();
}

Estructura del proyecto

  1. Abrir una terminal y crear una nueva solución en blanco con el comando
dotnet new sln -o MiApp

Esto es por si en algún momento quieres usar Visual Studio 2019 aca puedes ver las características mas importantes de esta version. Esto creara una carpeta llamada MiApp que contendrá el proyecto que tiene la siguiente estructura. En la carpeta src colocaremos todo el código de la aplicación y en la carpeta test el código de pruebas unitarias.

 MiApp
├───src
│   └───MiApp
└───test
    └───MiAppTest
  1. Crear un proyecto biblioteca de clases:
dotnet new classlib -o MiApp/src/MiApp

Este proyecto es donde se escribirá el código de la aplicación. Algunos autores lo llamas Sistema Bajo Prueba. Aquí es donde debe estar la clase estática Utilidades con el método invierte cadena.

  1. Crear un proyecto de pruebas con la plantilla xUnit. El argumento -o especifica la carpeta del proyecto.
dotnet new xunit -o MiApp/test/MiAppTest

Esta plantilla contiene añade al archivo de proyecto .csproj los paquetes de Nuget necesarios para poder codificar y ejecutar pruebas unitarias con xunit. En algunas ocaciones las versiones de estos paquetes pueden estar desactualizadas. Puedes usar la herramienta global dotnet-oudated para actualizarlos mediante la linea de comandos.

<ItemGroup>
  <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
  <PackageReference Include="xunit" Version="2.4.0" />
  <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
</ItemGroup>
  1. Agregar una referencia al proyecto de la aplicación en en el proyecto de pruebas.
dotnet add MiApp\test\MiAppTes reference MiApp\src\MiApp
  1. Agregar proyectos a solución
dotnet sln MiApp add MiApp\src\MiApp
dotnet sln MiApp add MiApp\test\MiAppTest

Implementar ciclo Red , Green , Refactor

Primero empezamos a crear las pruebas. Cambiamos el nombre de la clase UnitTest por InvierteCadenaTest y escribimos nuestra primera prueba.

[Fact]
public void DebeSerNulo()
{
  string entrada = null;
  var result =  Utilidades.InvierteCadena(entrada);
  Assert.Null(result);
}

Una prueba unitaria en xunit es un método decorado con el atributo [Fact] o [Theory] del espacio de nombres Xunit que regresa void. Esta prueba cubre el primer requerimiento descrito anteriormente. Una prueba unitaria generalmente usa el patrón 3A o en ingles Arrange Act Assert donde en la etapa

Etapa Detalles
Arrange Se establecen las precondiciones y entradas necesarias para la prueba
Act Se invoca el método o porción de código a probar.
Assert Se afirma que los resultados esperados han ocurrido

Con esto en mente escribimos las siguientes dos pruebas del requerimiento. Que sigue el mismo patrón que el método anterior.

[Fact]
public void DebeSerVacio()
{
  string entrada = string.Empty;
  var result =  Utilidades.InvierteCadena(entrada);
  Assert.Equal(string.Empty,result);
}

[Fact]
public void DebeSerIgual()
{
  string entrada = "A";
  var result =  Utilidades.InvierteCadena(entrada);
  Assert.Equal(entrada,result);
}

Para el ultimo requerimiento usaremos una prueba con el atributo [Theory] que básicamente estable un conjunto de valores de entrada y resultado esperado integrados en el atributo [InlineData()].

[Theory]
[InlineData("Hola","aloH")]
[InlineData("oso", "oso")]
[InlineData("hola", "aloh")]
[InlineData("yo Programo", "omargorP oy")]
[InlineData("Omar", "ramO")]
public void InvertirConjuntoDeCadenas(string entrada,string valorEsperado)
{
    var result =  Utilidades.InvierteCadena(entrada);
    Assert.Equal(valorEsperado,result);
}

Con esto podemos estamos en la primera etapa del ciclo Red-Green-Refactor ejecutando las pruebas y viéndolas fallar

dotnet test

Salida del comando dotnet test

La primeras 3 pruebas se pueden pasar con el siguiente código. La salida del comando dotnet test.

if (string.IsNullOrEmpty(entrada) || entrada.Length == 1)
{
  return entrada;
}
else
{
  throw new NotImplementedException();
}

Salida del comando dotnet test

El código que puede pasar todas las pruebas el siguiente.

public static string InvierteCadena(string entrada)
{

  if (string.IsNullOrEmpty(entrada) || entrada.Length == 1)
  {
    return entrada;
  }
  else
  {
    var letras = entrada.ToCharArray();
    string salida = string.Empty;
    for (int indice = letras.Length; indice > 0; indice--)
    {
      salida += letras[indice - 1];
    }
    return salida;
  }
}

Salida del comando dotnet test

Finalmente puedes refactorizar el codigo para aumentar la calidad del mismo. Por ejemplo usando el refactoring sugerido de Visual Studio conocido como Invertir if

Salida del comando dotnet test

Referencias