DEV Community

Cover image for Introducción a xUnit: Una poderosa herramienta para Unit Testing en .NET
Manuel Dávila
Manuel Dávila

Posted on

Introducción a xUnit: Una poderosa herramienta para Unit Testing en .NET

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 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
Enter fullscreen mode Exit fullscreen mode

Imagen de ejecución de comando dotnet --version

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
Enter fullscreen mode Exit fullscreen mode

Imagen de ejecución de comando dotnet new classlib -n FileModule -o FileModule

Dentro del directorio creado llamado FileModule, creamos una clase llamada FileManager con el siguiente código:

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Imagen de la estructura de carpetas y archivos del proyecto FileModule

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
Enter fullscreen mode Exit fullscreen mode

Imagen de ejecución de comando dotnet new xunit -n FileModule.UnitTests -o FileModule.UnitTests

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

Imagen de la estructura de carpetas y archivos del proyecto FileModule y FileModule.UnitTests

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
Enter fullscreen mode Exit fullscreen mode

Imagen de ejecución de comando dotnet add FileModule reference FileModule.UnitTests

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:

namespace FileModule.UnitTests;

public class UnitTest1
{
    [Fact]
    public void Test1()
    {
        Assert.False(false);
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Imagen de ejecución de comando dotnet test FileModule.UnitTests

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(() => { ... }): 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:

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)