Migrando de MSTest a xUnit

¡Hola!
Pues como bien dice el título, vamos a empezar a ver una serie de entradas para intentar pasar de MSTest a xUnit, viendo qué cosas cambian y por dónde empezar.
La primera pregunta que nos deberíamos hacer es, ¿deberíamos cambiar a xUnit? Aquí la respuesta es (como en casi todo), depende. En un proyecto ya iniciado en el que tengamos todos nuestros test con MSTest y no tengamos necesidades imperiosas para cambiar, por ejemplo que necesitemos sí o sí alguna característica de xUnit imposible de hacer con MSTest (imposible.. seguro¿?), es muy probable no merezca la pena el cambio. En un proyecto nuevo la cosa cambia; parece que xUnit (o NUnit) tienen más aprobación que MSTest, cuando digo más aprobación me refiero a que es lo que se aconseja por parte de la comunidad. Y no es excusa que el equipo de desarrollo siempre haya trabajado con MSTest porque el cambio es bastante natural. En cualquier caso, si no tienes necesidades muy complejas, seguramente con MSTest vayas servido. Al final lo importante es hacer test, independientemente del framework que utilicemos, porque nos ayudan a desarrollar aplicaciones más robustas, más fácil de mantener y extender, nos proporcionan documentación, etc.
Lo primero que deberíamos hacer es generar dos proyectos de test, en uno añadir el paquete de xUnit y en el otro de MSTest. Vemos un ejemplo básico de cada uno de ellos:

1
2
3
4
5
6
7
8
9
    [TestClass]
    public class DummyMSTest
    {
        [TestMethod]
        public void SampleTest()
        {
            Assert.IsTrue(true);
        }
    }
1
2
3
4
5
6
7
8
    public class DummyXUnitTest
    {
        [Fact]
        public void SampleTest()
        {
            Assert.True(true);
        }
    }

Poca diferencia. En xUnit no se utiliza el atributo [TestClass] para decorar la clase, porque xUnit sabe que es una clase de test porque tiene un test. Y el atributo que indica que un método es de test pasa de [TestMethod] a [Fact]. También el assert es algo diferente, pero eso lo veremos en otro momento.
Por ahora poca cosa… Vamos a empezar a ver diferencias un poco más profundas.
¿Cómo manejar ciclo de vida de un test? Aquí la cosa cambia un poco. En MSTest teníamos ciertos atributos para especificar métodos que se ejecutan antes de cada test ([TestInitialize]), después de cada test ([TestCleanup]), una sola vez por clase de test y antes de ejecutar ningún test de la clase ([ClassInitialize]), y una sola vez por clase de test y después de ejecutar todos los test de la clase ([ClassCleanup]). Lo vemos en el siguiente ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
    [TestClass]
    public class DummyMSTest
    {
        [ClassInitialize]        public static void ClassInitializeMethod(TestContext context)
        {
            System.Diagnostics.Debug.Write("ClassInitialize - Se ejecuta UNA SOLA VEZ por clase de test. ANTES de ejecutar ningún test.");
        }
 
        [ClassCleanup]        public static void ClassCleanupMethod()
        {
            System.Diagnostics.Debug.Write("ClassCleanupMethod - Se ejecuta UNA SOLA VEZ por clase de test. DESPUÉS de ejecutar todos los test.");
        }
 
        [TestInitialize]        public void TestInitializeMethod()
        {
            System.Diagnostics.Debug.Write("TestInitializeMethod - Se ejecuta ANTES de cada test.");
        }
 
        [TestCleanup]        public void TestCleanupMethod()
        {
            System.Diagnostics.Debug.Write("TestCleanupMethod - Se ejecuta DESPUÉS de cada test.");
        }
 
 
        [TestMethod]
        public void SampleTest()
        {
            Assert.IsTrue(true);
        }
 
        [TestMethod]
        public void OtherSampleTest()
        {
            Assert.IsTrue(true);
        }
    }

Si miramos la salida, es la siguiente:
1. ClassInitialize – Se ejecuta UNA SOLA VEZ por clase de test. ANTES de ejecutar ningún test.
2. TestInitializeMethod – Se ejecuta ANTES de cada test.
3. TestCleanupMethod – Se ejecuta DESPUÉS de cada test.
4. TestInitializeMethod – Se ejecuta ANTES de cada test.
5. TestCleanupMethod – Se ejecuta DESPUÉS de cada test.
6. ClassCleanupMethod – Se ejecuta UNA SOLA VEZ por clase de test. DESPUÉS de ejecutar todos los test.
Hay que fijarse en que los métodos decorados con los atributos [ClassInitialize] y [ClassCleanup] son métodos estáticos y [ClassInitialize] debe incluir un parámetro de tipo TestContext. La verdad que ser estáticos, no es un punto a favor de MSTest…
En xUnit el enfoque es un poco diferente, intenta evitar los atributos. Vamos a dividirlo en dos partes. Primero nos centramos en buscar la equivalencia de [TestInitialize] y [TestCleanup]; realmente no la hay, pero podemos hacer lo siguiente.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    public class DummyXUnitTest : IDisposable    {
        public DummyXUnitTest()        {
            System.Diagnostics.Debug.Write("Constructor - Se ejecuta ANTES de cada test.");
        }
 
        public void Dispose()        {
            System.Diagnostics.Debug.Write("Dispose - Se ejecuta DESPUÉS de cada test.");
        }
 
        [Fact]
        public void SampleTest()
        {
            Assert.True(true);
        }
 
 
        [Fact]
        public void OtherSampleTest()
        {
            Assert.True(true);
        }        
    }

La salida sería la siguiente:
1. Constructor – Se ejecuta ANTES de cada test.
2. Dispose – Se ejecuta DESPUÉS de cada test.
3. Constructor – Se ejecuta ANTES de cada test.
4. Dispose – Se ejecuta DESPUÉS de cada test.
Si nos fijamos bien, estamos diciendo que la clase del test sea IDisposable y hacemos uso del constructor y del método Dispose para simular los métodos a ejecutar antes de cada test y después. Esto es debido a que, por cada test que se ejecuta, xUnit crea una nueva instancia de la clase de test. Realmente xUnit no propone herramientas más allá del uso del constructor y el Dispose porque cree que generalmente no es una buena práctica utilizar este tipo de técnicas, y propone que cada clase de test tenga su propio contexto. Con respecto a esto no tengo una opinión formada, sí que es verdad que muchas veces es útil poder ejecutar código antes y después de cada test sin entrar en mayor complejidad (crear varias clases de test adicionales cada una con su propio contexto), en cualquier caso, tenemos la herramienta del constructor/dispose siempre a nuestra disposición.
Para simular[ClassInitialize] y [ClassCleanup] la cosa cambia un poco más, y hay que utilizar IClassFixture. Lo que vamos a hacer es mover la responsabilidad de inicialización del contexto de la clase del test a una clase externa.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    public class DummyFixture : IDisposable
    {
        public DummyFixture()
        {
            System.Diagnostics.Debug.Write("DummyFixture Constructor - Se ejecuta UNA SOLA VEZ por clase de test. ANTES de ejecutar ningún test.");
        }
 
        public void Dispose()
        {
            System.Diagnostics.Debug.Write("DummyFixture Dispose - Se ejecuta UNA SOLA VEZ por clase de test. DESPUÉS de ejecutar todos los test.");
        }
 
        public bool DummyBoolean { get; } = true;
    }

Ahora es responsabilidad de la clase DummyFixture la creación/destrucción del contexto. En el constructor inicializaríamos lo que necesitemos y en el método Dispose destruimos lo necesario. Aquí expondríamos, por ejemplo, propiedades que serían usadas desde nuestra clase de test. El siguiente paso es hacer que nuestra clase de test utilice DummyFixture mediante IClassFixture<>.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    public class DummyXUnitTest : IClassFixture<DummyFixture>    {
        private readonly DummyFixture fixture;
 
        public DummyXUnitTest(DummyFixture fixture)        {
            this.fixture = fixture;
        }
 
        [Fact]
        public void SampleTest()
        {
            Assert.True(fixture.DummyBoolean);
        }
 
        [Fact]
        public void OtherSampleTest()
        {
            Assert.True(fixture.DummyBoolean);
        }        
    }

La salida sería:
1. DummyFixture Constructor – Se ejecuta UNA SOLA VEZ por clase de test. ANTES de ejecutar ningún test.
2. DummyFixture Dispose – Se ejecuta UNA SOLA VEZ por clase de test. DESPUÉS de ejecutar todos los test.
Es el propio xUnit el que se encarga de mantener la misma instancia de nuestro fixture en la ejecución de los diferentes test de la clase DummyXUnitTest.
Podríamos hacer que nuestra clase de test (DummyXUnitTest) implemente varios IClassFixture<>, lo único que debemos hacer es añadir al constructor de la clase los correspondientes argumentos por cada Fixture.
Típicamente utilizaremos esta técnica cuando la creación/destrucción de nuestro contexto de la clase de test sea demasiado costosa (en tiempo), como para crearlo con cada ejecución de test, por ejemplo cosas relacionadas con bases de datos.
Si realmente la construcción/destrucción es muy costosa, podemos utilizar una última herramienta que nos proporciona xUnit y no está presente (por lo menos de forma directa) en MSTest, y es ICollectionFixture<>. Esta herramienta nos proporciona la posibilidad de compartir un mismo contexto para diferentes clases de test, o dicho con otras palabras, se crea el contexto una única vez antes de ejecutar los test que lo implementen y se destruye cuando finalicen todos los test que lo implementen. Vemos el código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    public class DummyFixture : IDisposable
    {
        public DummyFixture()
        {
            System.Diagnostics.Debug.Write("DummyFixture Constructor - Se ejecuta una sola vez antes de la ejecución de los test.");
        }
 
        public void Dispose()
        {
            System.Diagnostics.Debug.Write("DummyFixture Dispose - Se ejecuta UNA SOLA VEZ después de la ejecución de los test.");
        }
 
        public bool DummyBoolean { get; } = true;
    }
 
    [CollectionDefinition("DummyFixture collection")]    public class DatabaseCollection : ICollectionFixture<DummyFixture>    {
    }

La clase DummyFixture la mantenemos igual, pero hemos añadido una nueva clase DatabaseCollection que implementa ICollectionFixture y está decorada con el atributo [CollectionDefinition(“DummyFixture collection”)]. El nombre que incluimos dentro del atributo CollectionDefinition debe ser único. A continuación se ve cómo se implementa en nuestras dos clases de test.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    [Collection("DummyFixture collection")]    public class DummyXUnitTest
    {
        private readonly DummyFixture fixture;
 
        public DummyXUnitTest(DummyFixture fixture)
        {
            this.fixture = fixture;
        }
 
        [Fact]
        public void SampleTest()
        {
            Assert.True(fixture.DummyBoolean);
        }
    }
 
    [Collection("DummyFixture collection")]    public class OtherDummyXUnitTest
    {
        private readonly DummyFixture fixture;
 
        public OtherDummyXUnitTest(DummyFixture fixture)
        {
            this.fixture = fixture;
        }
 
        [Fact]
        public void OtherSampleTest()
        {
            Assert.True(fixture.DummyBoolean);
        }
    }

Utilizamos el atributo Collection junto con el nombre para especificar que queremos utilizar nuestra CollectionDefinition. Si ejecutamos todos los test (remarco, son dos clases de test), la salida es:
1. DummyFixture Constructor – Se ejecuta una sola vez antes de la ejecución de los test.
2. DummyFixture Dispose – Se ejecuta UNA SOLA VEZ después de la ejecución de los test.
La instancia de DummyFixture se ha mantenido en la ejecución de todos los test, y el método Dispose, como era de esperar, sólo se ha llamado una vez.
Hemos dado un repaso a los atributos básicos y cómo es el ciclo de vida de los test. En las siguientes entradas veremos otras cosas interesantes para adoptar xUnit desde MSTest.
¡Un saludo!

Otros post:
Migrando de MSTest a xUnit
Migrando de MSTest a xUnit. Atributos
Migrando de MSTest a xUnit. Test parametrizados

2 thoughts on “Migrando de MSTest a xUnit”

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *