Como usar xUnit y Fluent Assertions en .NET Core 2.x
En esta entrada vamos a ver como utilizar xUnit y Fluent Assertions en .NET Core.
Por hacer un pequeño recordatorio, xUnit es una herramienta gratuita y de código abierto que nos permitirá realizar pruebas unitarias en nuestro código.
Aunque lo podemos utilizar en .NET Framework, también podemos utilizarlo en .NET Core.
En realidad, el uso de xUnit en .NET Framework y .NET Core es prácticamente el mismo.
Por otro lado, Fluent Assertions hace que las Asserts sean utilizadas de forma más natural a como lo haríamos en el lenguaje humano de forma que nuestros tests puedan ser comprendidos y leídos de forma más clara.
Te recomiendo leer la entrada Unit Testing y el Patrón AAA si tienes alguna duda respecto al Patrón AAA que utilizaré en este ejemplo.
Al igual que xUnit, Fluent Assertions puede ser utilizado con .NET Framework y con .NET Core.
Y de igual manera, su uso en .NET Framework y .NET Core es prácticamente el mismo también.
También conviene mencionar que Fluent Assertions soporta no sólo xUnit, sino un montón de frameworks de pruebas unitarias como por ejemplo MsTest o NUnit por citar algunos de los más habituales en el mundo .NET.
Este proyecto de ejemplo nos explicará como trabajar con xUnit, Fluent Assertions y .NET Core de forma sencilla.
En primer lugar, vamos a crear un proyecto de .NET Core.
En mi caso utilizando Visual Studio 2017.
Para ello, he utilizado la plantilla xUnit Test Project (.NET Core).
Una vez creado nuestro proyecto, lo primero que haremos será abrir NuGet sobre nuestro proyecto e instalar el paquete de Fluent Assertions.
La plantilla de xUnit para Visual Studio 2017 por su parte, instalará dos paquetes NuGet relacionados con xUnit.
A continuación podremos escribiremos el código de nuestra aplicación.
El código lo he dividido en cuatro ficheros.
Tres de ellos tendría que ver con la «lógica» que quiero probar.
El último corresponde con los tests (5 en mi caso).
Aunque lo habitual es probar un ensamblado y no tener todo en el mismo proyecto, he decidido hacerlo así sólo para llevar a cabo nuestras pruebas de forma práctica.
El código de la parte «lógica» a probar es el siguiente:
MissingPersonNameException.cs
using System; namespace xUnitFluentAssertions.UnitTests { public class MissingPersonNameException : Exception { public MissingPersonNameException() : base("Name is missing") { } } }
WrongPersonAgeException.cs
using System; namespace xUnitFluentAssertions.UnitTests { public class WrongPersonAgeException : Exception { public WrongPersonAgeException(int age) : base($"Age {age} is invalid. Should be greater than 18.") { } } }
Person.cs
using System; namespace xUnitFluentAssertions.UnitTests { public class Person { public string Name { private get; set; } public int Age { private get; set; } public Person(string name, int age) { if (age < 18) throw new WrongPersonAgeException(age); this.Name = name; this.Age = age; } public override string ToString() { if (String.IsNullOrEmpty(this.Name)) throw new MissingPersonNameException(); return $"Hi {this.Name}!"; } public string GetName() => this.Name; public int GetAge() => this.Age; } }
Si nos paramos un poco a mirar el código de estas tres clases, veremos que la última, la clase Person, es la que tiene en sí la parte más global a probar.
Así que la última clase que tendremos, la que corresponde con el test en sí, la he enfocado a la clase Person.
El código de esta clase queda de la siguiente forma:
PersonTests.cs
using FluentAssertions; using System; using Xunit; namespace xUnitFluentAssertions.UnitTests { public class PersonTests { private readonly string _name = "Jorge"; private readonly int _age = 18; [Fact] public void Person_WithConstructorValues_ShouldBeNameAsExpected() { // Arrange var person = new Person(this._name, this._age); // Act var personName = person.GetName(); // Assert personName.Should().Be(this._name); } [Fact] public void Person_WithConstructorValues_ShouldBeAgeAsExpected() { // Arrange var person = new Person(this._name, this._age); // Act var personAge = person.GetAge(); // Assert personAge.Should().Be(this._age); } [Fact] public void Person_WithConstructorValuesAndUsingToString_ShouldBeAsExpected() { // Arrange var person = new Person(this._name, this._age); // Act var personToString = person.ToString(); // Assert personToString.Should().StartWith("Hi").And.EndWith("!").And.Contain(this._name).And.ContainAll($"Hi {this._name}!"); } [Fact] public void Person_WithMissingName_ShouldReturnsAnException() { // Arrange var person = new Person(String.Empty, this._age); // Act Action action = () => person.ToString(); // Assert person.GetAge().Should().Be(this._age); action.Should().Throw<MissingPersonNameException>().WithMessage("Name is missing"); } [Fact] public void Person_WithWrongAge_ShouldReturnsAnException() { // Arrange & Act var age = 17; Action action = () => new Person(String.Empty, age); // Assert action.Should().Throw<WrongPersonAgeException>().WithMessage($"Age {age} is invalid. Should be greater than 18."); } } }
Y aquí empezaremos a explicar todo lo relativo a xUnit y Fluent Assertions, ya que esta última clase es la que lo usa.
Aunque explicaré de forma breve los ejemplos de test que he hecho, recuerda que la documentación es siempre nuestra aliada.
Podrás acceder a la documentación de Fluent Assertions en este enlace.
Fluent Assertions necesita los símbolos de depuración o debug symbols para encontrar sobre todo los detalles de código con respecto a posibles problemas o fallos en los tests.
De ahí que la compilación de los tests deba hacerse en modo Debug incluso en el sistema que tengamos preparado para compilar nuestra solución.
Las pruebas que he preparado consiste en validar el objeto Person.
Las pruebas básicas tienen que ver con la clase Person y sus propiedades, en las que validamos en cierta manera la inmutabilidad de la información y nos aseguramos que no ha sido alterada al crearse.
Otra prueba tiene por objeto recibir una información de la clase Person y validar que realmente se está generando correctamente tal y como esperamos. Sin Fluent Assertions deberíamos crear tantos Assert como partes del resultado quisiéramos probar, pero en este caso y con una única instrucción, estaremos probando todos los posibles casos esperados.
Sin embargo, he querido complicar un poco las validaciones y he optado por levantar dos excepciones según determinadas circunstancias. Una proactiva al instanciar Person y otra reactiva al llamar a un método de la clase Person. En estos dos casos, preparamos una acción sobre la cuál vamos a preguntar si se ha producido una excepción del tipo esperado e incluso vamos a validar el mensaje que debería tener.
Como vemos, el uso de Fluent Assertions lo estamos comenzando con Should(), y a partir de ahí y según lo que deseemos validar, empezaremos a anidar las validaciones.
El método Should() es la base de Fluent Assertions, y es la puerta de entrada para validar el resultado esperado.
Recuerda compilar la solución antes de abrir la ventana de tests para poder lanzarlos.
Nuestros tests en ejecución se muestran como se indica en la siguiente imagen.
Podrás acceder al código de estos Tests en mi repositorio de GitHub en este enlace.
Happy Coding!