Test de aprobación con ApprovalTests. Reporters

¡Hola!
Si en la anterior entrada vimos los conceptos básicos de ApprovalTests, hoy vamos a echar un vistazo a los Reporters. Los Reporters son las herramientas que tenemos disponibles para tomar las decisiones cuando un test falla y nos sirven principalmente poder visualizar el resultado del test fallido, comparar los cambios que se han producido entre la versión correcta y la versión actual fallida, y decidir si los cambios fallidos en realidad son correctos.
El otro día vimos un test como el siguiente:

1
2
3
4
5
6
7
8
9
10
    [UseReporter(typeof(DiffReporter))]
    public class ApprovalBasicTest
    {
        [Fact]
        public void ValidateTicketFormat_Test()
        {
            string ticket = GivenATicket();
            Approvals.Verify(ticket);
        }
    }

En este test, estamos definiendo un Reporter de tipo DiffReporter que, básicamente, va a buscar el primer programa que tengas disponible en tu ordenador para mostrarte las diferencias (siempre que el test falle y deban mostrarse diferencias claro). En la versión que yo estoy utilizando (3.0.14), el listado de programas que va a buscar son (en Windows):

  1. CodeCompare.exe
  2. Beyond Compare 3\\BCompare.exe
  3. Beyond Compare 4\\BCompare.exe
  4. TortoiseMerge.exe
  5. TortoiseGitMerge.exe
  6. TortoiseIDiff.exe
  7. Araxis Merge\\Compare.exe
  8. p4merge.exe
  9. WinMergeU.exe
  10. kdiff3.exe
  11. devenv.exe
  12. Assert.AreEqual de MSTest
  13. Assert.AreEqual de NUnit
  14. Assert.Equal de XUnit 2
  15. Assert.Equal de XUnit
  16. Debug.WriteLine(message) // Console.WriteLine(message);

Como comento, va a utilizar la primera de las funcionalidades que tenga disponibles. A mí, personalmente, me ha parecido bastante curioso utilizar los Asserts de las diferentes librerías de testing (no se me hubiera ocurrido); lo que hace es, cuando se tiene que lanzar el Reporter porque el valor ha cambiado, mediante reflection utiliza el Assert de la librería en concreto (XUnit, MSTest…). Seguramente no sea la más útil y es algo frágil porque tiene las funciones harcodeadas, por ejemplo => Xunit.Assert, xunit.assert», «Equal», aunque son funciones bastante estables y seguramente no vayan a cambiar, pero me han parecido curiosos 🙂
En el anterior código estamos definiendo un único Reporter, pero se pueden definir todos los Reports que queramos añadiendo más parámetros al atributo UseReporter:

1
2
3
4
5
6
7
8
9
10
    [UseReporter(typeof(DiffReporter),typeof(ClipboardReporter))]    public class ApprovalBasicTest
    {
        [Fact]
        public void ValidateTicketFormat_Test()
        {
            string ticket = GivenATicket();
            Approvals.Verify(ticket);
        }
    }

Como el atributo UseReporter admite un número variable de argumentos (con params), los separamos por comas y listo.
Ahora bien, estamos definiendo el Reporter a nivel de clase de test y puede que nos interese hacerlo de otra forma, bien a nivel de método porque así lo requerimos o a nivel más general. Para ello ApprovalTests nos proporciona varias formas de definir el Reporter y su alcance. Se puede definir individualmente a nivel de método como se ve a continuación:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    public class ApprovalBasicTest
    {
        [UseReporter(typeof(QuietReporter))]        [Fact]
        public void ValidateTicketFormat_Test()
        {
            string ticket = GivenATicket();
            Approvals.Verify(ticket);
        }
 
        [UseReporter(typeof(DiffReporter))]        [Fact]
        public void ValidateTicketFormat_Test_With_DiffReporter()
        {
            string ticket = GivenATicket();
            Approvals.Verify(ticket);
        }
    }

Otra opción es definirlo a nivel de ensamblado. Para ello abrimos el archivo AssemblyInfo.cs (cuelga de Properties de nuestro proyecto de test) y añadimos, por ejemplo, un DiffReporter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    using System.Reflection;
    using System.Runtime.InteropServices;
    using ApprovalTests.Reporters;
 
    [assembly: AssemblyTitle("Approval.Test")]
    [assembly: AssemblyDescription("")]
    [assembly: AssemblyConfiguration("")]
    [assembly: AssemblyCompany("")]
    [assembly: AssemblyProduct("Approval.Test")]
    [assembly: AssemblyCopyright("Copyright ©  2018")]
    [assembly: AssemblyTrademark("")]
    [assembly: AssemblyCulture("")]
    [assembly: UseReporter(typeof(DiffReporter))] 
    [assembly: ComVisible(false)]
 
    [assembly: Guid("0cd93366-3a0c-42de-b7b3-523e6f52f48a")]
 
    // [assembly: AssemblyVersion("1.0.*")]
    [assembly: AssemblyVersion("1.0.0.0")]
    [assembly: AssemblyFileVersion("1.0.0.0")]

Personalmente esta opción no me gusta demasiado porque me parece que queda demasiado oculta y si no tienes conocimiento de la herramienta, es posible que no sepas muy bien qué está pasando. Aunque seguramente sea culpa mía porque no utilizo habitualmente el archivo de información de ensamblado y sería de los últimos sitios donde miraría. Otra opción para definirlo a nivel de ensamblado y que no quede tan oculto, es crear un archivo especifico para ello al que le demos un nombre más expresivo. En el código fuente de ApprovalTests le llaman ApprovalTestsConfig.cs, es decir, creamos un nuevo archivo .cs, le llamamos por ejemplo ApprovalTestsConfig y añadimos lo mismo que antes:

1
2
3
    using ApprovalTests.Reporters;
 
    [assembly: UseReporter(typeof(DiffReporter))]

Por último, tenemos la opción de añadirlo como un appSettings en el archivo app.config del proyecto de test:

1
2
3
    <appSettings>
        <add key="UseReporter" value="ApprovalTests.Reporters.DiffReporter, ApprovalTests"/>
    </appSettings>

Y a continuación, especificamos que se utilice el Reporter de la configuración (lo hacemos por ejemplo a nivel de clase):

1
2
3
4
5
6
7
8
9
10
    [UseReporter(typeof(AppConfigReporter))]    public class ApprovalBasicTest
    {
        [Fact]
        public void ValidateTicketFormat_Test()
        {
            string ticket = GivenATicket();
            Approvals.Verify(ticket);
        }
    }

Esta opción puede ser muy interesante si utilizamos integración continua y en cada commit (o cuando sea) ejecutamos los test, ya que seguramente no tenga demasiado sentido utilizar los Reports que abren programas (como TortoiseIDiff), sino que seguramente nos interese más utilizar Reports básicos (como QuietReporter) porque sólo queremos saber si los test pasan o no. Definiéndolo así (en el app.config), tendremos nuestra configuración de UseReporter en nuestra máquina local, y en el servidor tendremos otra que se ajuste más a este entorno.
Un detalle importante e interesante, es que se va a utilizar siempre el Reporter más cercano al test. Si por ejemplo tenemos un Reporter especificado a nivel de ensamblado, otro a nivel de clase de test y otro a nivel de un método de test, se va a utilizar el del método del test. Además, en caso de que no se especifique Reporter en alguno de los diferentes niveles, se va a utilizar el del padre más próximo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    [UseReporter(typeof(QuietReporter))]
    public class ApprovalBasicTest
    {
        [Fact]        public void ValidateTicketFormat_Test()        {            string ticket = GivenATicket();            Approvals.Verify(ticket);        } 
        [UseReporter(typeof(DiffReporter))]
        [Fact]
        public void ValidateTicketFormat_Test_With_DiffReporter()
        {
            string ticket = GivenATicket();
            Approvals.Verify(ticket);
        }
    }

En el caso del test que se resalta (ValidateTicketFormat_Test), como no tiene Reporter especificado, se utiliza el de su padre más próximo, que en este caso está definido en la clase de test (ApprovalBasicTest) y sería QuietReporter.
Ahora vamos a dar un repaso por algunos de los diferentes Reporters que tenemos disponibles (hay muchos).

QuietReporter

Este quizá sea el más básico de todos. Simplemente va a mostrar por consola el comando que necesitamos ejecutar para sobrescribir el resultado actual por el anterior, es decir, si ejecutamos nuestro test y éste falla pero el nuevo valor decidimos que es el correcto, nos va a mostrar el comando que necesitamos ejecutar para utilizar el nuevo archivo generado por el test (el archivo received), como valor correcto (archivo approved). Por ejemplo:

1
2
3
4
5
6
7
8
9
10
    public class ApprovalBasicTest
    {
        [UseReporter(typeof(QuietReporter))]        [Fact]
        public void ValidateTicketFormat_Test()
        {
            string ticket = GivenATicket();
            Approvals.Verify(ticket);
        }       
    }

Si el test anterior falla, y echamos un vistazo a la consola, veremos algo así:

cmd /c move /Y "C:\Users\Luis\Source\repos\Approval.Test\Approval.Test\Approval.Test\ApprovalBasicTest.ValidateTicketFormat_Test.received.txt" "C:\Users\Luis\Source\repos\Approval.Test\Approval.Test\Approval.Test\ApprovalBasicTest.ValidateTicketFormat_Test.approved.txt"

Si ejecutamos lo anterior en la consola:
Hemos aceptado el valor que ha generado el test como valor correcto, por lo que, si volvemos a ejecutar el test éste se pondrá en verde.

ClipboardReporter

Este Reporter funciona básicamente igual que el anterior, pero en vez de mostrar en la consola el comando para establecer como aprobado el valor generado por el test, directamente lo copia al portapapeles, así que, una vez el test haya fallado y queramos aceptar el resultado, basta con abrir la consola, pegar y pulsar intro. Este Reporter lo utilizaremos junto con otros (recordamos que se pueden añadir varios Reporters a UseReporter) que nos muestren el resultado de forma visual, porque realmente por sí solo, ClipboardReporter no nos aporta feedback del resultado.

AllFailingTestsClipboardReporter

Este interesante Reporter funciona igual que ClipboardReporter, pero con la diferencia de que acumula los comandos. Por ejemplo tenemos la siguiente clase de test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    [UseReporter(typeof(AllFailingTestsClipboardReporter))]    public class ApprovalBasicTest
    {
        [Fact]
        public void ValidateTicketFormat_Test()
        {
            string ticket = GivenATicket();
            Approvals.Verify(ticket);
        }
 
        [Fact]
        public void ValidateSimplifiedTicketFormat_Test()
        {
            string simplifiedticket = GivenASimplifiedTicket();
            Approvals.Verify(simplifiedticket);
        }
    }

Si ambos test fallan nos va a copiar al portapapeles los comandos necesarios para aceptar ambos tests (archivos) como válidos:

cmd /c move /Y "C:\Users\Luis\Source\repos\Approval.Test\Approval.Test\Approval.Test\ApprovalBasicTest.ValidateTicketFormat_Test.received.txt" "C:\Users\Luis\Source\repos\Approval.Test\Approval.Test\Approval.Test\ApprovalBasicTest.ValidateTicketFormat_Test.approved.txt"
cmd /c move /Y "C:\Users\Luis\Source\repos\Approval.Test\Approval.Test\Approval.Test\ApprovalBasicTest.ValidateSimplifiedTicketFormat_Test.received.txt" "C:\Users\Luis\Source\repos\Approval.Test\Approval.Test\Approval.Test\ApprovalBasicTest.ValidateSimplifiedTicketFormat_Test.approved.txt"

Esta opción puede ser muy interesante si no nos interesa subir los archivos aprobados a nuestro repositorio. Decoramos todas las clases con los UserReporter que queramos y también añadimos éste y, de esta manera, quien se descargue por primera vez el proyecto del repositorio sólo tendría que pasar todos los tests (fallarían) y ejecutar en la consola los comandos que se hayan copiado para que la siguiente vez que se ejecuten no fallen. Esta solución tiene algunos problemas. Si en algún test en concreto utilizamos un Reporter diferente, estamos obligados a añadir también AllFailingTestsClipboardReporter porque como dijimos, siempre se utilizan los Reporters más cercano al test que se está ejecutando. Además, si tenemos varios proyectos diferentes de test, debemos ejecutar (la primera vez) los proyectos de forma individual y copiar los comandos en la consola, puesto que se van a ejecutar en contextos diferentes y se pierde el valor de la propiedad estática que tiene AllFailingTestsClipboardReporter y almacena los valores de los comandos.

DiffReporter

Al principio de la entrada hemos visto un poco el funcionamiento. Se basa en buscar la primera herramienta que tengas disponible y ésa será la que se utilice. Por ejemplo, yo voy a instalar en mi equipo TortoiseIDiff que, aunque parece una herramienta un poco arcaica, funciona bastante bien con ApprovalTests. Ejecutamos el siguiente test:

1
2
3
4
5
6
7
8
9
10
    public class ApprovalBasicTest
    {
        [UseReporter(typeof(DiffReporter))]        [Fact]
        public void ValidateTicketFormat_Test()
        {
            string ticket = GivenATicket();
            Approvals.Verify(ticket);
        }       
    }

En el momento que nos falle y, puesto que he instalado TortoiseIDiff, se va a abrir el programa:
En la parte de la izquierda se muestra el valor que se ha generado al ejecutar el test y en la derecha el valor que estaba actualmente aprobado. Como vemos, se observan bien las diferencias generadas, en concreto son en la línea 7. Aceptar el valor generado como valor aprobado es muy sencillo con esta herramienta, tan solo tenemos que hacer click con el botón derecho en la parte de la izquierda (el nuevo valor generado), seleccionar «Use this whole file» y guardar.
Realmente DiffReporter no provee más funcionalidad que escoger y utilizar algunos de los Reporters concretos, en el caso de TortoiseIDiff, utiliza internamente TortoiseDiffReporter.
En la próxima entrada seguiremos viendo algunos Reporters más.

¡Un saludo!

One thought on “Test de aprobación con ApprovalTests. Reporters”

Deja un comentario

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