Introducción a xUnit: Una poderosa herramienta para Unit Testing en .NET
Tags: testing, dotnet, xunit, spanish
Published: 2023-06-12 02:32:51


xUnit es una tecnología muy popular para hacer pruebas unitarias (unit test) en aplicaciones .NET y en este artículo daremos los primeros pasos. 

Esta herramienta se puede usar con C#, F# o Visual Basic. Además, es Open Source, simple y flexible.

## Pasos previos a la configuración de xUnit

Es importante que tengas el [SDK .NET 7](https://dotnet.microsoft.com/en-us/download) instalado y tu editor de código preferido, en mi caso utilizaré Visual Studio Code. Si ya lo tienes instalado, crea un directorio y desde la terminal ubicate en la ruta de dicho directorio. Luego, ejecuta el siguiente comando: 
```
dotnet --version
```

![Imagen de ejecución de comando dotnet --version](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/42uj8sobdsu8ajwe0znb.jpg)

Para poder hacer testing, es necesario tener funcionalidades a probar, pueden ser clases o métodos. Por lo tanto, primero vamos a crear un proyecto simple de biblioteca de clases con el comando:
```
dotnet new classlib -n FileModule -o FileModule
```

![Imagen de ejecución de comando dotnet new classlib -n FileModule -o FileModule](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sdz25au8tg73p48y085y.jpg)

Dentro del directorio creado llamado FileModule, creamos una clase llamada FileManager con el siguiente código:
```csharp
namespace FileModule
{
    public class FileManager
    {
        private readonly string directory;

        public FileManager(string directory)
        {
            this.directory = directory;
        }

        public string ReadFile(string fileName)
        {
            if(string.IsNullOrWhiteSpace(fileName))
                throw new ArgumentNullException();
                
            string filePath = GetFilePath(fileName);

            if (File.Exists(filePath))
                return File.ReadAllText(filePath);
            
            return string.Empty;
        }

        public void CreateFile(string fileName)
        {
            string filePath = GetFilePath(fileName);
            File.Create(filePath).Close();
        }

        private string GetFilePath(string fileName)
        {
            return Path.Combine(directory, fileName);
        }
    }
}
```

![Imagen de la estructura de carpetas y archivos del proyecto FileModule](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pa9o2tyknngz88kuzzor.jpg)

## Configuración inicial: Integrando xUnit en tu proyecto

Una vez finalizado el paso anterior, es momento de crear el proyecto de xUnit para escribir y ejecutar nuestras pruebas unitarias. Para ello puedes ejecutar el siguiente comando:
```
dotnet new xunit -n FileModule.UnitTests -o FileModule.UnitTests
```

![Imagen de ejecución de comando dotnet new xunit -n FileModule.UnitTests -o FileModule.UnitTests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5qpbq01zb8xz7p534i76.jpg)

Tu solución debería estar de la siguiente forma:

![Imagen de la estructura de carpetas y archivos del proyecto FileModule y FileModule.UnitTests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oit9fzt9c89h44aear5g.jpg)

A continuación, vamos agregar la referencia del proyecto FileModule a FileModule.UnitTests para poder crear las pruebas unitarias de los métodos con el comando:
```
dotnet add FileModule.UnitTests reference FileModule
```

![Imagen de ejecución de comando dotnet add FileModule reference FileModule.UnitTests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hoxbvhbrhax3nhxojhfj.jpg)

En la clase UnitTest1 que se crea por defecto, colocar el siguiente código con la finalidad de observar la ejecución de una prueba:
```csharp
namespace FileModule.UnitTests;

public class UnitTest1
{
    [Fact]
    public void Test1()
    {
        Assert.False(false);
    }
}
```

Las pruebas unitarias no deben retornar valores, es decir deben ser void.

Para ejecutar las pruebas unitarias del proyecto, debemos colocar el siguiente comando:
```
dotnet test FileModule.UnitTests
```

![Imagen de ejecución de comando dotnet test FileModule.UnitTests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgbdmljozq65m7uldyb9.jpg)

Intenta cambiar el false por el true y ejecuta nuevamente el comando, obtendrás una prueba fallida.

Con esto acabas de realizar tu primera prueba unitaria en xUnit.

## Estructura básica de un test en xUnit

### Atributos

Como observaste en el ejemplo anterior usamos el atributo **Fact**, el cual sirve para marcar un método como prueba unitaria. Algunos otros atributos más usados:

- **Theory**: Se utiliza para marcar un método como una teoría, las cuales son pruebas parametrizadas que se ejecutan con diferentes conjuntos de datos que se pasan a través del atributo InlineData.
- **InlineData**: Se utiliza en combinación con Theory y permite proporcionar datos para pruebas parametrizadas. Puedes pasar valores directamente a través del atributo, como *[InlineData(1, "Maad")]*, o usar argumentos de tipo simple, como *[InlineData("MaadCode")]*.
- **MemberData**: Se utiliza para pasar datos a una teoría desde una propiedad estática o un campo en la misma clase de prueba o en una clase externa.

### Aserciones

Las aserciones sirven para verificar el comportamiento esperado en las pruebas unitarias. Algunos assert más comunes en xunit:

- Assert.Equal(expected, actual): Esta aserción verifica si dos valores son iguales. Es útil para comparar el valor esperado con el valor real obtenido durante la prueba.
- Assert.True(condition): Comprueba si una condición es verdadera. Se utiliza para validar expresiones booleanas y asegurarse de que se cumplan ciertas condiciones en una prueba.
- Assert.False(condition): Comprueba si una condición es falsa. Al igual que Assert.True(), se utiliza para validar expresiones booleanas y asegurarse de que no se cumplan ciertas condiciones en una prueba.
- Assert.Null(object): Verifica si un objeto es nulo. Se utiliza para asegurarse de que un objeto esperado no tenga ningún valor asignado.
- Assert.NotNull(object): Comprueba si un objeto no es nulo. Es útil para asegurarse de que un objeto esperado tenga un valor asignado.
- Assert.Throws<Exception>(() => { ... }): Verifica si se lanza una excepción durante la ejecución de una acción determinada. Se utiliza para asegurarse de que un código específico genere una excepción esperada.
- Assert.InRange(actual, min, max): Comprueba si un valor se encuentra dentro de un rango determinado. Es útil para validar que un resultado se encuentre en un rango específico.
- Assert.Collection(expectedCollection, actualCollection): Verifica si dos colecciones contienen los mismos elementos en el mismo orden. Se utiliza para comparar dos colecciones y asegurarse de que sean idénticas.

Es momento de aplicarlo a nuestra prueba. Para eso debemos renombrar el UnitTest1 por FileModuleTest y Test1 por ReadFile_ExistingFile_ReturnsFileContent con el siguiente código:
```csharp
using FileModule;

namespace FileModule.UnitTests;

public class FileModuleTest
{
    [Fact]
    public void ReadFile_ExistingFile_ReturnsFileContent()
    {
        // Arrange
        var directory = "TestDirectoryFiles";
        var fileNameOriginal = "prueba.txt";
        var content = "Unit Testing with xUnit!";

        Directory.CreateDirectory(directory);
        string filePath = Path.Combine(directory, fileNameOriginal);
        File.WriteAllText(filePath, content);
        
        var fileName = "prueba.txt";
        FileManager fileManager = new FileManager(directory);
        // Act
        string result = fileManager.ReadFile(fileName);
        // Assert
        Assert.Equal(content, result);
    }

    [Fact]
    public void ReadFile_FileNameIsNull_throwArgumentException()
    {
        // Arrange
        var directory = "TestDirectoryFiles";
        var fileNameOriginal = "prueba.txt";
        var content = "Unit Testing with xUnit!";

        Directory.CreateDirectory(directory);
        string filePath = Path.Combine(directory, fileNameOriginal);
        File.WriteAllText(filePath, content);
        
        string? fileName = null;
        FileManager fileManager = new FileManager(directory);
        // Act y Assert
        Assert.Throws<ArgumentNullException>(() => fileManager.ReadFile(fileName));
    }

    [Fact]
    public void ReadFile_FileNameIsEmpty_throwArgumentException()
    {
        // Arrange
        var directory = "TestDirectoryFiles";
        var fileNameOriginal = "prueba.txt";
        var content = "Unit Testing with xUnit!";

        Directory.CreateDirectory(directory);
        string filePath = Path.Combine(directory, fileNameOriginal);
        File.WriteAllText(filePath, content);
        
        var fileName = "";
        FileManager fileManager = new FileManager(directory);
        // Act y Assert
        Assert.Throws<ArgumentNullException>(() => fileManager.ReadFile(fileName));
    }

    [Fact]
    public void ReadFile_FileNameIsWhiteSpace_throwArgumentException()
    {
        // Arrange
        var directory = "TestDirectoryFiles";
        var content = "Unit Testing with xUnit!";
        var fileNameOriginal = "prueba.txt";
        Directory.CreateDirectory(directory);
        string filePath = Path.Combine(directory, fileNameOriginal);
        File.WriteAllText(filePath, content);
        
        var fileName = " ";
        FileManager fileManager = new FileManager(directory);
        // Act y Assert
        Assert.Throws<ArgumentNullException>(() => fileManager.ReadFile(fileName));
    }
}
```

Aquí estamos aplicando la estrategia triple A (Arrange, Act, Assert) y la técnica de Naming de *MethodName_Scenario_ExpectedBehaviour* para el nombrado de nuestras pruebas.

Sin embargo, este código esta realizando la misma configuración previa en cada test, el cual consiste en crear el directorio y el archivo con el texto. Por lo tanto, eso lo podemos realizar en el constructor. En otras bibliotecas como NUNit tendrías que usar el atributo SetUp.
```csharp
using FileModule;

namespace FileModule.UnitTests;

public class FileModuleTest
{
    private string _directory = "TestDirectoryFiles";
    public FileModuleTest() {
        var fileName = "prueba.txt";
        var content = "Unit Testing with xUnit!";

        Directory.CreateDirectory(_directory);
        string filePath = Path.Combine(_directory, fileName);
        File.WriteAllText(filePath, content);
    }

    [Fact]
    public void ReadFile_ExistingFile_ReturnsFileContent()
    {
        // Arrange
        var contentExpected = "Unit Testing with xUnit!";
        var fileName = "prueba.txt";
        FileManager fileManager = new FileManager(_directory);
        // Act
        string result = fileManager.ReadFile(fileName);
        // Assert
        Assert.Equal(contentExpected, result);
    }

    [Fact]
    public void ReadFile_FileNameIsNull_throwArgumentException()
    {
        // Arrange
        string? fileName = null;
        FileManager fileManager = new FileManager(_directory);
        // Act y Assert
        Assert.Throws<ArgumentNullException>(() => fileManager.ReadFile(fileName));
    }

    [Fact]
    public void ReadFile_FileNameIsEmpty_throwArgumentException()
    {
        // Arrange
        var fileName = "";
        FileManager fileManager = new FileManager(_directory);
        // Act y Assert
        Assert.Throws<ArgumentNullException>(() => fileManager.ReadFile(fileName));
    }

    [Fact]
    public void ReadFile_FileNameIsWhiteSpace_throwArgumentException()
    {
        // Arrange
        var fileName = " ";
        FileManager fileManager = new FileManager(_directory);
        // Act y Assert
        Assert.Throws<ArgumentNullException>(() => fileManager.ReadFile(fileName));
    }

    [Fact]
    public void ReadFile_NonExistentFile_ReturnEmptyString()
    {
        // Arrange
        var fileName = "otherfile.txt";
        FileManager fileManager = new FileManager(_directory);
        // Act
        string result = fileManager.ReadFile(fileName);
        // Assert
        Assert.Equal(string.Empty, result);
    }
}
```

Ahora ejecuta el comando ```dotnet test FileModule.UnitTests``` en la consola y comprueba que pasen todos los tests.

Ten en cuenta que no sólo se prueban los métodos que retornan un tipo de dato. Por lo tanto, te reto a crear las pruebas unitarias para el método CreateFile.

## xUnit para diferentes casos de prueba

Como pudiste ver, en muchas de nuestras pruebas unitarias se espera que se lance una Excepción. Para ello, podemos usar los atributos Theory e InlineData para agrupar casos de prueba que tienen el mismo comportamiento esperado. El código quedaría de la siguiente forma:
```csharp
using FileModule;

namespace FileModule.UnitTests;

public class FileModuleTest
{
    private string _directory = "TestDirectoryFiles";
    public FileModuleTest() {
        var fileName = "prueba.txt";
        var content = "Unit Testing with xUnit!";

        Directory.CreateDirectory(_directory);
        string filePath = Path.Combine(_directory, fileName);
        File.WriteAllText(filePath, content);
    }

    [Fact]
    public void ReadFile_ExistingFile_ReturnsFileContent()
    {
        // Arrange
        var contentExpected = "Unit Testing with xUnit!";
        var fileName = "prueba.txt";
        FileManager fileManager = new FileManager(_directory);
        // Act
        string result = fileManager.ReadFile(fileName);
        // Assert
        Assert.Equal(contentExpected, result);
    }

    [Theory]
    [InlineData(null)]
    [InlineData(" ")]
    [InlineData("")]
    public void ReadFile_FileNameIsNullOrWhiteSpace_throwArgumentException(string fileName)
    {
        // Arrange
        FileManager fileManager = new FileManager(_directory);
        // Act y Assert
        Assert.Throws<ArgumentNullException>(() => fileManager.ReadFile(fileName));
    }

    [Fact]
    public void ReadFile_NonExistentFile_ReturnEmptyString()
    {
        // Arrange
        var fileName = "otherfile.txt";
        FileManager fileManager = new FileManager(_directory);
        // Act
        string result = fileManager.ReadFile(fileName);
        // Assert
        Assert.Equal(string.Empty, result);
    }
}
```

## Consideraciones

- Utiliza estrategias como triple A (Arrange, Act, Assert) para tus pruebas unitarias.
- Debes tener una técnica de Naming para tus clases y métodos de prueba. Algunos prefieren nombrar a sus clases de prueba con la palabra *Should* al final, en nuestro caso usamos *Test*. Lo importante es mantener dicha técnica con todo el equipo y en todo el proyecto.
- Debes identificar lo que debes probar y lo que no.
- Tus pruebas unitarias deben ser cortas y deben probar sólo una pequeña porción de funcionalidad.
- Existen herramientas en los IDE como Visual Studio, que facilitan la ejecución, depuración y visualización de pruebas unitarias.

Espero que este post te sea de utilidad y lo hayas disfrutado. 
¡Happy coding!