viernes, 21 de febrero de 2014

Introducción a NUnit (III): Ciclo de vida de un test

En este tercer post de la serie veremos qué atributos hay disponibles en NUnit para configurar la inicialización y destrucción de datos requeridos por los tests.

 

Antes de comenzar sólo un par de tips para saber dónde y cómo capturar cierta salida de tracing de un test para poder seguir la pista al orden en que los atributos son aplicados.

 

Si por ejemplo escribimos el siguiente test:

 

[Test]

public void Prueba()

{

    System.Diagnostics.Debug.WriteLine("panicoenlaxbox");

    Assert.Pass();

}

¿Dónde aparecerá la información de tracing?

 

En ReSharper directamente en la salida:

 

clip_image001

 

En Test Explorer, pulsando en el enlace “Salida”

 

clip_image002

 

En NUnit.exe en la pestaña “Text Output”, siempre y cuando hayamos activado la opción “Gui\Text Output\Trace Output” (por defecto desactivado).

 

clip_image003

 

En NUnit 3.0 deberemos usar TestContext.Write/WriteLine https://github.com/nunit/docs/wiki/TestContext#write, mientras que en xUnit.net hay que tirar algo de código para usar ITestOutputHelper https://xunit.github.io/docs/capturing-output.html.

 

Una vez que tenemos localizado donde aparecerá la información de tracing, es momento de ponerse manos a la obra y llenar nuestro código de información de traza al más viejo y rudimentario estilo ASP clásico con Response.Write ¿Te acuerdas?... Ya sé que no es muy digno, pero es la opción más sencilla :-)

 

Los atributos que permiten gestionar la inicialización y destrucción de datos en los test son:

  • SetUp
  • TearDown
  • TestFixtureSetUp
  • TestFixtureTearDown

SetUp marca un método para que se ejecute antes de cada test.

 

TearDown marca un método para que se ejecute después de cada test.

 

TestFixtureSetUp marca un método para que se ejecute una sola vez antes de que se ejecute cualquier test en un fixture.

 

TestFixtureTearDown marca un método para que ejecute una sola vez cuando hayan finalizado todos los tests de un fixture.

 

Por ejemplo, para el siguiente código:

 

    class CalculadoraText

    {

        [SetUp]

        public void InicializarTest()

        {

            System.Diagnostics.Trace.WriteLine("SetUp");

        }

 

        [TearDown]

        public void LiberarTest()

        {

            System.Diagnostics.Trace.WriteLine("TearDown");

        }

 

        [TestFixtureSetUp]

        public void InicializarFixture()

        {

            System.Diagnostics.Trace.WriteLine("TestFixtureSetUp");

        }

 

        [TestFixtureTearDown]

        public void LiberarFixture()

        {

            System.Diagnostics.Trace.WriteLine("TestFixtureTearDown");

        }

 

        [Test]

        public void Test1()

        {

            System.Diagnostics.Trace.WriteLine("Test1");

            Assert.Pass();

        }

 

        [Test]

        public void Test2()

        {

            System.Diagnostics.Trace.WriteLine("Test2");

            Assert.Pass();

        }

    }

Esta es la salida:

 

clip_image004

 

Otro atributo interesante es SetUpFixture. Este atributo decora una clase cuyos métodos decorados con SetUp y TearDown serán ejecutados una sola vez dentro del espacio de nombres. Lógicamente, en esta clase no tiene sentido (tampoco serán reconocidos) agregar ningún método de test. Este atributo podría servir para organizar fixtures relacionados con tareas de inicialización compartidas bajo un mismo espacio de nombres

 

[SetUpFixture]

class PreparacionTests

{

    [SetUp]

    public void Inicializar()

    {

        System.Diagnostics.Trace.WriteLine("SetUpFixture SetUp");

    }

 

    [TearDown]

    public void Liberar()

    {

        System.Diagnostics.Trace.WriteLine("SetUpFixture TearDown");

    }

}

clip_image005

 

Si hay varios métodos decorados con estos atributos se ejecutarán todos ellos (por ejemplo podríamos llegar a esa situación si heredemos de una clase base que ya incorpora métodos decorados).

 

En NUnit 3.0 este ciclo de vida ha cambiado https://github.com/nunit/docs/wiki/SetUp-and-TearDown-Changes.

 

Si lo comparamos con xUnit.net, en este último el ciclo de vida es más sencillo. Para empezar se crea una instancia de clase de tests por cada test ejecutado, siendo así, con el constructor y el método Dispose (si es necesario teardown) nos bastaría. Para compartir estado entre tests de la misma clase usaríamos Class Fixtures - IClassFixture<> y para compartir estado entre distintas clases usaríamos Collection Fixtures - ICollectionFixture<> https://xunit.github.io/docs/shared-context.html.

 

Llegados a este punto, podrías pensar que si cada método de test requiere su propia configuración de inicialización y destrucción (no compartida con el resto de tests y asumiendo que no quieres escribir el código de estas tareas en el propio test) las soluciones aquí expuestas no parecen la mejor opción. Claro está que podrías hacer una clase por test, pero tampoco parece una opción viable. Es por ello que NUnit nos brinda otra vía distinta de personalización (de nuevo basada en atributos) para configurar de forma exclusiva la configuración que un test requiere. La idea está en sacar fuera del test la inicialización y destrucción, creando un nuevo atributo que herede de TestActionAttribute y sobrescriba los métodos BeforeTest y AfterTest, es lo que NUnit llama atributos de acción.

 

class SetupParaTest1Attribute : TestActionAttribute

{

    public override void BeforeTest(TestDetails testDetails)

    {

        System.Diagnostics.Trace.WriteLine("BeforeTest");

    }

 

    public override void AfterTest(TestDetails testDetails)

    {

        System.Diagnostics.Trace.WriteLine("AfterTest");

    }

}

 

class CalculadoraText

{

    [Test]

    [SetupParaTest1]

    public void Test1()

    {

        System.Diagnostics.Trace.WriteLine("Test1");

        Assert.Pass();

    }

}

clip_image006

 

Otra propiedad interesante de TestActionAttribute y que se puede sobrescribir es ActionTargets. Con esta propiedad indicamos cómo se comportará el atributo de acción en función de si decora un método de test o un fixture.

 

Los posibles valores son:

  • Default (por defecto).
  • Test.
  • Suite.

Con Default, si se pone en un test se ejecutará antes y después del test, si se pone en un fixture se ejecutará una sola vez para todos los test del fixture.

 

Con Test depende de a quien decore. Si se pone a un método de test se ejecutará antes y después del test, pero si se pone a un fixture se ejecutar una vez para cada uno de los test del fixture.

 

Con Suite sólo se aplicará a nivel de fixture (una sola vez para todos los tests) y si decora a un test, no fallara pero no hará nada.

 

Lógicamente, algunos pensarán que montar todo este tinglado para el setup de un test es mucha parafernalia, ¿Por qué no simplemente incluir código en la sección “Arrange” del test? Pues parece más legible, desde luego… al final los detractores de los atributos siempre esgrimirán el argumento de que no favorecen la lectura del código y quizás nos les falte razón.

 

Como añadido, si queremos saber el directorio actual donde se está ejecutando el test (para crear algún fichero por ejemplo), con NUnit usaremos TestContext.TestDirectory https://github.com/nunit/docs/wiki/TestContext#testdirectory mientras que xUnit.net no nos brinda ningún método de ayuda y tendremos que obtener esta información a mano con un código como el siguiente:

private string GetTestDirectory()
{
    var codebase = new Uri(Assembly.GetExecutingAssembly().CodeBase);
    return Path.GetDirectoryName(codebase.LocalPath);
}

Si estamos en .NET Core, el código sería el siguiente:

private string GetTestDirectory()
{
    var codebase = new Uri(typeof(YourTypeGoesHere).GetTypeInfo().Assembly.CodeBase);
    return Path.GetDirectoryName(codebase.LocalPath);
}

Después de ver este post, te adelanto que ya sólo queda uno pendiente en la serie y será en el que hablemos sobre los test parametrizados.

 

Si te preguntas si es mejor usar NUnit o xUnit, valga este reddit para decirte que da igual https://www.reddit.com/r/csharp/comments/4198ei/should_i_use_nunit_or_xunit/ 
“It doesn't matter. NUnit 3 is available now which has a bunch of nice new features, being a complete rewrite. XUnit continues to innovate. Neither will change your life for the better (or worse) in any dramatic way. Spend an hour with each and then pick the one that feels most intuitive. Or just pick XUnit if you want the current populist choice. It really doesn't matter.”

 

Mas post de esta serie:


Un saludo!