Saltar a contenido

Unit Testing .Net

Pre-requisitos

  1. Leer las buenas prácticas para el desarrollo de Unit Test

Introducción

Para crear un Test en .Net debemos:

  • Agregar un proyecto a la solución.

Agregar

  • Seleccionar el proyecto de UnitTest

Crear

En .Net podemos seleccionar el proyecto con xUnit o NUnit para realizar los test, son dos librerías que permiten realizar los test. Los ejemplos de esta documentación estan realizados con xUnit. Docs xUnit y NUnit.

  • Referenciar el proyecto a testear

Crear

Crear

Basic

Para comenzar a realizar un test debemos crear una clase.

Los test no son otra cosa que funciones de una clase.

Para identificar un test debemos agregar el atributo [Fact], este es definido por la librería xUnit.

        [Fact]
        public void Account_ValidCustomerName()
        {
        }
Se recomienda que el nombre de la función debe describir lo que se intenta probar

Sugerimos esta nomenclatura:

metodo_escenario_resultado

Donde el

  • método: es el metodo que se va a ejecutar
  • escenario: la condición que se va probar
  • resultado: el resultado esperado de la acción.

Ejemplo:

  • Credit_WithValidAmount_UpdatesBalance()
  • Debit_WithValidAmount_UpdatesBalance()
  • Handle_CreatePerson_Success()

Primer test

        [Fact]
        public void Account_ValidCustomerName()
        {
            // Arrange
            double beginningBalance = 11.99;
            string name = "Mr. Bryan Walton";
            BankAccount account = new BankAccount(name, beginningBalance);

            // Act
            var result = account.CustomerName;

            // Assert
            Assert.Equal(name, result);
        }
En la función anterior podemos detectar las 3 partes de la regla de la triple A

  • Arrange: construimos el escenario, creamos un objeto account que será el que validaremos.
  • Act: ejecutamos el método que queremos probar.
  • Assert: Validamos la respuesta del método.

El Objeto Assert es el objeto que nos permite validar nuestro test, debemos especificar que es lo que queremos validar y en caso que no se cumpla, Assert se encargará de marcar el test como fail y de especificar el motivo del fallo.

Run Test

Para correr nuestro test en visual studio utilizaremos el Explorador de Pruebas o Test Explorer.

Test Explorer

Con los botones podemos correr parcial o totalmente nuestros test e incluso debugear.

En caso que querramos correr en consola debemos ejecutar

 dotnet test

FluentAssertions

Es una libreria que permite realizar las comprobaciones de una manera más visual.

Ejemplo:

        [Fact]
        public void Account_ValidCustomerName()
        {
            // Arrange
            double beginningBalance = 11.99;
            string name = "Mr. Bryan Walton";
            BankAccount account = new BankAccount(name, beginningBalance);

            // Act
            var result = account.CustomerName;

            // Assert
            result.Should().Be(name);
        }

Advanced

Theory

Es posible que se haya preguntado por qué sus primeras pruebas unitarias usan un atributo con nombre [Fact] en lugar de uno con un nombre más tradicional como Test. xUnit.net incluye soporte para dos tipos principales diferentes de pruebas unitarias: Fact y Theory. Al describir la diferencia entre hechos y teorías, nos gusta decir:

Los hechos son pruebas que siempre son verdaderas. Prueban condiciones invariantes.

Las teorías son pruebas que sólo son verdaderas para un conjunto particular de datos.

Un buen ejemplo de esto es probar algoritmos numéricos. Supongamos que desea probar un algoritmo que determina si un número es impar o no. Si está escribiendo las pruebas del lado positivo (números impares), entonces introducir números pares en la prueba haría que fallara, y no porque la prueba o el algoritmo sean incorrectos.

[Theory]
[InlineData(3)]
[InlineData(5)]
[InlineData(6)]
public void MyFirstTheory(int value)
{
    Assert.True(IsOdd(value));
}

bool IsOdd(int value)
{
    return value % 2 == 1;
}
Resultado:

Theory Result

Moq

Moq es una librería que permite generar objetos "Mock" que sustituyan a las dependencias de nuestra clase a probar. Puede repasar la documentación para ver estos conceptos.

Por Ejemplo, podemos generar un mock de las dependencias de un Handler

var _query = new Mock<IQuerySqlServer>();

var _handler = new ListPersonHandler(_query.Object);
Esto nos crea un objeto ListPersonHandler la dependencia de consultas a base de datos "moqueada"

También podemos manipular ese objeto Mock para que se comporte como necesitamos en el Test. Por ejemplo, podemos pedir que la función que busque en base de datos devuelva una lista de objetos que creamos con anterioridad.

Esto se consigue con la función Setup del Mock.

Ejemplo:

[Fact]
public async Task Handle_GetListUser_Success()
{
    // Arrange
    var request = new ListPerson();
    var resonse = new Fixture().CreateMany<PersonDto>();
    // se configura el Mock
    _query.Setup(_ => _.GetAllAsync<PersonDto>(It.IsAny<string>()))
        .ReturnsAsync(resonse);
    // Act
    var result = await _handler.Handle(request, _cancellationToken);

    // Assert
    result.Content.Should().Equal(resonse);
    result.StatusCode.Should().Be(HttpStatusCode.OK);
}
Para ver más funcionamiento de la librería Moq consultar: Moq Quickstart

AutoFixture

AutoFixture es una librería que permite generar un objeto de prueba. Se recomienda usar cuando los objetos que debemos crear para preparar la prueba sean grandes o varios y no tenga validación del contenido del mismo ya que AutoFixture los genera con información inválida.

Otro ejemplo:

[Fact]
public async Task Handler_UpdatePerson_ThrowUpdateDatabase()
{
    // Arrange
    var request = new Fixture().Create<UpdatePersonCommand>();
    var person = new Fixture().Create<Person>();
    _query.Setup(_ => _.GetByIdAsync<Person>(It.IsAny<string>(), It.IsAny<string>()))
        .ReturnsAsync(person);
    _repository.Setup(_ => _.SaveChangeAsync()).ThrowsAsync(new DbUpdateException());

    // Act
    // Assert
    await Assert.ThrowsAsync<DbUpdateException>(() => _handler.Handle(request, _cancellationToken));

}
En este ejemplo estamos configurando el Mock para que falle con una exception.

Para ver más funcionalidades sobre AutoFixture ver: Guide AutoFixture

Como Mockear el DbContext

Para testear nuestro servicio QuerySqlServer y mockear el DbContext, debemos crear una clase base con el nombre ServiceDbContextBaseTest y utilizar la configuración de la base de datos en memoria de EF Core. Aquí tienes un ejemplo:

public class ServiceDbContextBaseTest
{
    protected ApplicationDbContext _inMemoryDbContext;

    public ServiceDbContextBaseTest()
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .ConfigureWarnings(x => x.Ignore(InMemoryEventId.TransactionIgnoredWarning))
            .UseInMemoryDatabase(Guid.NewGuid().ToString("N"), b => b.EnableNullChecks(false)).Options;
        _inMemoryDbContext = new(options);
        _inMemoryDbContext.Database.EnsureDeleted();
        _inMemoryDbContext.Database.EnsureCreated();
    }
}

Una vez que hemos configurado el contexto, heredamos de ServiceDbContextBaseTest y estamos listos para testear nuestras funciones. En este ejemplo, vamos a testear el método GetPersonByNameAsync. Creamos un conjunto de datos, añadimos y guardamos los cambios con el contexto y validamos los datos. Aquí tienes un ejemplo:

    public class QuerySqlServerTest : ServiceDbContextBaseTest
    {
        private readonly Mock<IReadOnlyQueryConfiguration> _config;
        private readonly QuerySqlServer _query;
        public QuerySqlServerTest() : base() 
        {
            _config = new();
            _query = new(_config.Object, _inMemoryDbContext);
        }

        [Fact]
        public async Task GetPersonByNameAsync_Returns_Person_By_Name()
        {
            // Arrange
            List<Person> people = [
                new Person
                {
                    PersonId = 4,
                    Apellido = "Messi",
                    Nombre = "Lionel"
                },
                new Person
                {
                    PersonId= 5,
                    Apellido= "Son",
                    Nombre= "Goku"
                }
            ];

            _inMemoryDbContext.Person.AddRange(people);
            await _inMemoryDbContext.SaveChangesAsync();

            // Act
            var result = await _query.GetPersonByNameAsync("Lionel");

            // Assert
            Assert.NotNull(result);
            Assert.Equal(people[0], result);
        }

     }

CodeCoverage

Platform

La herramienta platform-cli está equipada con una funcionalidad integrada que facilita el análisis automático de la cobertura de código (code coverage). Ver más sobre el funcionamiento y compatibilidad

Ejemplo basico
  1. Abrimos una terminal.
  2. Nos posicionamos en nuestra carpeta root o la que posee el archivo *.sln (Solucion)
  3. Ejecutamos
terminal
platform test

Obtendremos el siguiente resultado:

cmd web

Configuración Manual

Si no contamos con compatibilidad con platform-cli podemos hacer la configuración manual en nuestros proyectos.

Configuración

Install ReportGenerator

ReportGenerator es una herramienta de código abierto utilizada en el desarrollo de software para convertir informes de cobertura de código de varias herramientas de prueba en informes legibles y comprensibles. Es ampliamente utilizada en entornos de integración continua y para análisis detallados de la cobertura de pruebas.

terminal
dotnet tool install -g dotnet-reportgenerator-globaltool

Packages Test

Devemos incorporar a nuestros projectos de test el nuget coverlet.msbuild

coverlet msbuild

terminal
dotnet add package coverlet.msbuild --version 6.0.0

Configurar .gitignore

Agregar las siguientes exclusiones al archivo .gitignore

.gitignore
# coverage
*coverage.opencover.xml
*coverage.cobertura.xml
**/coveragereport/*

Run test

El siguiente comando ejectua los test y genera los archivos de cobertura.

terminal
dotnet test --collect:\"XPlat Code Coverage\" /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura

Generar el reporte

Generamos el reporte

terminal
reportgenerator -reports:test\*\*.cobertura.xml 
                -targetdir:coveragereport 
                -reporttypes:Html 
                -classfilters:-WebApi.Controllers.*;-Startup;-Program;-*.Domain.Entities.*;-*Dto*;-*DependencyInjection;-*Context;-*.Infrastructure.Migrations.*;-*.Infrastructure.Persistence.Configurations.*;-*Entity;-*Event;-*Vm;-*Exception
Abrimos el archivo index.html ubicado en la carpeta coveragereport

Ejemplos

Se dispone un repo con varios ejemplos de test para que puedan guiarse en los primeros pasos. GitHub/Architecture-it

Autor/a: Olivera Lucas

Contacto: lolivera@andreani.com