Escribiendo tests con xUnit usando BeforeAfterTestAttribute

xUnit es una gran framework de test para .NET, una de sus principales ventajas son las características de extensibilidad que tiene.
En este artículo vamos a ver de que forma vamos a poder utilizar el atributo BeforeAfterTestAttribute para conseguir tener unos test muchos más legibles en algunos escenarios.

Estructura de un atributo personalizado

Para crearnos un atributo personalizado tenemos que heredar de BeforeAfterTestAttribute y sobreescribir los métodos Before y After.

public class ExampleAttribute: BeforeAfterTestAttribute

public override void Before (System.Reflection.MethodInfo methodUnderTest)  
{
}

public override void After (System.Reflection.MethodInfo methodUnderTest)  
{
}

En el método Before ponemos lo que queremos que se ejecute al principio de la ejecución del test. En el método After ponemos lo que queremos que se ejecute al finalizar el test.

Utilizando el atributo

Para utilizar un atributo de este tipo solo hay que decorar el test donde queremos utilizarlo junto al atributo Fact.

public class ExampleTests

[Fact, Example]
public void Test ()  
{
}

Ventajas

Hay varias ventajas que vamos a obtener si escribimos nuestros tests decorándolos con este tipo de atributo:

  • El código queda mucho más limpio, separamos la parte importante del test, que es lo que se prueba, de la parte de preparación y limpieza.
  • El método After se ejecuta incluso cuando se produce una excepción en el test.
  • Al ser un atributo donde queda la lógica de preparación y limpieza, se puede reutilizar en diferentes tests.

Ejemplos de uso

Ahora vamos a ver una serie de escenarios donde puede ser útil aplicar este tipo de atributo.

Trazas

Realizar trazas es un escenario donde encaja el uso de este atributo, pueden ser escribir trazas a fichero, base de datos, email, consola etc..

public class TraceAttribute : BeforeAfterTestAttribute  
{
    System.Diagnostics.Stopwatch watch;

    public override void Before(MethodInfo methodUnderTest)
    {
        watch = new System.Diagnostics.Stopwatch();
        watch.Start();
        Console.WriteLine(string.Format(
                "Before : {0}.{1}",
                methodUnderTest.DeclaringType.FullName,
                methodUnderTest.Name));
    }

    public override void After(MethodInfo methodUnderTest)
    {
        watch.Stop();
        Console.WriteLine(string.Format(
                "After : {0}.{1}",
                methodUnderTest.DeclaringType.FullName,
                methodUnderTest.Name));
    }
}

Base de datos

Cuando hacemos test de integración y hay una base de datos involucrada, podemos utilizar este atributo para crear y borrar la base de datos. También podemos utilizar este parámetro para para englobar las acciones contra la base de datos que se hacen dentro del test dentro de una transacción y después hacer un rollback.

public sealed class AutoRollbackAttribute : BeforeAfterTestAttribute  
    {
        TransactionScope scope;

        public TransactionScopeAsyncFlowOption AsyncFlowOption { get; set; } = TransactionScopeAsyncFlowOption.Enabled;

        public IsolationLevel IsolationLevel { get; set; } = IsolationLevel.Unspecified;

        public TransactionScopeOption ScopeOption { get; set; } = TransactionScopeOption.Required;

        public long TimeoutInMS { get; set; } = -1;

        public override void After(MethodInfo methodUnderTest)
        {
            scope.Dispose();
        }

        public override void Before(MethodInfo methodUnderTest)
        {
            var options = new TransactionOptions { IsolationLevel = IsolationLevel };
            if (TimeoutInMS > 0)
                options.Timeout = TimeSpan.FromMilliseconds(TimeoutInMS);

            scope = new TransactionScope(ScopeOption, options, AsyncFlowOption);
        }
    }

Conclusiones

xUnit tiene unas características de extensión que podemos aprovechar para escribir nuestros test de forma que las acciones como preparación y limpieza del test podemos dejarlas encapsuladas dentro de un BeforeAfterTestAttribute y así el test solo contiene el código realmente importante quedando más legible.