Los tests de aprobación (Approval Tests) son un tipo de test que se basan en aprovechar el conocimiento humano para validar el resultado de los tests en lugar de tener que escribir asserts como se hace en los tests unitarios normales.
Esto suena un poco raro, pero es realmente sencillo. La idea es que ejecutamos el código que queremos testear y observamos su salida, que puede ser texto, una imagen, un fichero, una página web… lo que sea. Una vez que tenemos la salida, una persona verifica que el resultado es correcto y lo aprueba (de ahí lo de tests de aprobación).
A partir de ese momento, cada vez que se ejecuta el test se compara la salida obtenida con la salida aprobada y, si no son iguales, el test falla. Cuando un test ha fallado, una persona debe decidir si hay que corregir el código o hay que aprobar el nuevo resultado.
Aunque a priori parece que este tipo de tests están limitados a clases que generen algún tipo de salida, usando herramientas como DumpToText o incluso un simple XmlSerializer podemos utilizarlos para realizar tests de estado sobre nuestros objetos, lo que hace este tipo de test bastante más versátil.
Herramientas para crear tests de aprobación
Para crear y ejecutar tests de aprobación existe una librería llamada, claro está, Approval Tests. La forma de trabajar con ella puede resultar un poco confusa al principio, así que vamos a ver un ejemplo sencillo:
public class Screen { private readonly List<Sprite> sprites = new List<Sprite>(); public Size Size { get; set; } public IEnumerable<Sprite> Sprites { get { return sprites; } } public void Add(Sprite sprite) { sprites.Add(sprite); } } public class Sprite { public Point Location { get; set; } public string Name { get; set; } } [TestFixture] public class MyTests { [Test] public void Add_Sprites_To_Screen() { var screen = new Screen {Size = new Size(100, 100)}; screen.Add(new Sprite {Name = "Zombie", Location = new Point(1, 1)}); screen.Add(new Sprite {Name = "Survivor", Location = new Point(2, 2)}); // Esta es la parte clave: pedimos la aprobación del resultado del test, // que en este caso es el volcado a string del objeto Screen Approvals.Approve(screen.DumpToTextValue()); } }
En este ejemplo tenemos una clase, Screen
, que puede almacenar Sprite
s, y un test para validar que realmente los almacena. La aprobación se realiza con el método Approvals.Approve
que usamos en la última línea del test, en la cual aprovechamos la librería DumpToText
para convertir el estado del Screen
en un string
y usarlo como texto a aprobar:
Approvals.Approve(screen.DumpToTextValue());
Cuando ejecutemos el test por primera vez, fallará con un mensaje de error parecido a éste:
move /Y "E:\Dev\SampleApp\MyTests.Add_Sprites_To_Screen.received.txt" "E:\Dev\SampleApp\MyTests.Add_Sprites_To_Screen.approved.txt" ApprovalTests.Core.Exceptions.ApprovalMissingException : Failed Approval: Approval File "E:\Dev\SampleApp\MyTests.Add_Sprites_To_Screen.approved.txt" Not Found.
Al ejecutar Approval.Approve(string str)
, se genera un fichero de salida con el nombre CLASE.MÉTODO.received.txt
y se compara con el fichero CLASE.MÉTODO.approved.txt
que contiene el resultado aprobado para ese test. Como todavía no hemos aprobado ningún resultado para el test, no se puede verificar el resultado y el test falla con una excepción ApprovalMissingException
.
Para aprobar el test deberemos revisar el contenido del fichero XXX.received.txt
y, si es correcto, renombrarlo a XXX.approved.txt
. Approval Tests nos facilita las cosas dejándonos junto al mensaje de error que hemos visto antes el comando que podemos ejecutar en una consola para aprobar el test:
move /Y "E:\Dev\SampleApp\MyTests.Add_Sprites_To_Screen.received.txt" "E:\Dev\SampleApp\MyTests.Add_Sprites_To_Screen.approved.txt"
Una vez que hemos aprobado el resultado, si volvemos a ejecutar el test podremos comprobar que el test finaliza con éxito. Si ahora cambiamos el método Add
de la clase Screen
para provocar un fallo:
public void Add(Sprite sprite) { // Comentamos para provocar un fallo // sprites.Add(sprite); }
Al ejecutar el test nos mostrará el siguiente error:
ApprovalTests.Core.Exceptions.ApprovalMismatchException : Failed Approval: Received file E:\Dev\SampleApp\MyTests.Add_Sprites_To_Screen.received.txt does not match approved file E:\Dev\SampleApp\MyTests.Add_Sprites_To_Screen.approved.txt.
En lugar del ApprovalMissingException
, ahora tenemos un ApprovalMismatchException
indicándonos que no coincide el fichero que hemos aprobado con el que se ha generado durante la ejecución del test.
En este punto, podríamos comparar a mano ambos ficheros para encontrar el problema y decidir si corregimos el código o aprobamos el nuevo resultado (renombrando el fichero XXX.received.txt
, como hicimos antes). Sin embargo, Approval Tests incluye utilidades para hacer esa tarea más sencilla: los Reporter
s.
Los Reporters
son clases que se encargan de mostrar el resultado de un test en caso de fallo para ayudarnos a decidir qué hacer. Approval Tests incluye varios tipos de Reporters listo para utilizar. En este caso, podemos emplear DiffReporter
, que nos mostrará la comparación entre el resultado aprobado y el resultado real del test usando Tortoise Diff:
[Test] [UseReporter(typeof(DiffReporter))] public void Add_Sprites_To_Screen() { var screen = new Screen {Size = new Size(100, 100)}; screen.Add(new Sprite {Name = "Zombie", Location = new Point(1, 1)}); screen.Add(new Sprite {Name = "Survivor", Location = new Point(2, 2)}); // Esta es la parte clave: pedimos la aprobación del resultado del test, // que en este caso es el volcado a string del objeto Screen Approvals.Approve(screen.DumpToTextValue()); }
Con esto es mucho más fácil comparar ambos resultados y decidir con cual nos quedamos.
Aunque en el ejemplo anterior hemos probado con un fichero de texto y Approval.Approve
está comparando string
s, existen otros métodos en Approval
e incluso otras clases como WinForms.Approval
que nos permiten trabajar con otros formatos, como documentos html, xml, archivos binarios o incluso capturas de pantalla (para testear aplicaciones de escritorio), por lo que podemos cubrir bastantes casos con tests de este estilo.