¡Hola!
Hoy vamos a dar un vistazo a una librería muy interesante para mejorar nuestros test, Autofixture. Esta librería nos va a permitir desacoplarnos de la etapa Arrange y poder refactorizar y añadir nuevas funcionalidades a nuestro código sin miedo a tener que tocar muchos test (a lo mejor para añadir un nuevo parámetro a un constructor), en resumen, mejorar la mantenibilidad de nuestros tests.
Supongamos que tenemos una clase como la siguiente:
1 2 3 4 5 6 7 8 9 | public class Employee { public string Name { get; set;} public Person(string name) { Name = name; } } |
Esta clase Employee se utiliza en cientos de test y seguramente en casi todos los test, simplemente sea un mal necesario para inicializar una clase, o se requiera como parámetro en algún sitio, pero realmente lo único que necesitamos sea tener una instancia de Employee con valores consistentes. Por ejemplo:
1 2 3 4 5 6 7 8 9 10 11 | [Fact] public void AddEmployeeToDepartment() { Employee employee = new Employee("María"); Department department = new Department(); department.AddEmployee(employee); var result = department.ContainsEmployee(employee); Assert.True(result); } |
Como podemos ver en el anterior test, únicamente creamos un empleado, lo añadimos a un departamento y comprobamos que el empleado está en el departamento. Más allá de lo tonto del test, lo importante aquí es darnos cuenta de que nuestro único objetivo es tener un empleado, nos da igual que sea «María» o «Pepe», es más, quizá en este ejemplo no se vea claro, pero si fuese por ejemplo un valor entero en vez de un string y le asignamos un valor 3, el que lea el test (seguramente nosotros en un futuro) puede pensar, ¿ese valor 3 es por algo en concreto?, ¿tiene que ser 3?, ¿podría ser 0? Creo que queda bastante claro que sólo queremos un empleado, no nos importan los detalles del empleado. El problema viene cuando creamos una instancia de Employee en un montón de test y llega el día en el que tenemos que hacer esto:
1 2 3 4 5 6 7 8 9 10 11 | public class Employee { public string Name { get; set;} public int Age { get; set;} public Employee(string name, int age) { Name = name; Age = age; } } |
Mal asunto, ahora tenemos un nuevo parámetro en nuestro constructor y muchos test que cambiar. Llegados a este punto podemos estar tentados a hacer trampa y no añadir el parámetro por constructor, pero no deberíamos. A lo mejor tenemos herramientas de refactorización en las que confiamos mucho y nos pueden ayudar a añadir el parámetro a todos los test donde tengamos una instancia de Employee. Aquí el problema está en que no nos deberíamos haber acoplado al constructor de Employee en nuestro test. Es muy posible que en nuestra base de código este constructor sólo lo utilicemos en contadas ocasiones, pero como vemos en nuestros test la cosa cambia. Para prevenir esto tenemos algunas opciones, por ejemplo, utilizar Object Mother y tener una clase para construir nuestros empleados:
1 2 3 4 5 6 7 8 9 10 11 12 | public static class TestEmployees { public static Employee Employee() { return new Employee("María", 23); } public static Employee UnderageEmployee() { return new Employee("María", 16); } } |
Esta clase nos provee de empleados y hace que cuando cambiemos los detalles de la creación de un empleado, sólo tengamos que cambiarlo en un punto. Dentro de nuestra clase de creación de empleados podemos añadir otros métodos que nos proporcionen empleados con características especiales, por ejemplo, un empleado menor de edad (aunque debería depender del país…). En el test lo veríamos así:
1 2 3 4 5 6 7 8 9 10 11 | [Fact] public void AddEmployeeToDepartment() { Employee employee = TestEmployees.Employee(); Department department = new Department(); department.AddEmployee(employee); var result = department.ContainsEmployee(employee); Assert.True(result); } |
Podemos ver que ahora nuestros test que requieren un empleado sólo dependen de un único punto. El problema del Object Mother es cuando la creación de nuestros objetos es más compleja, requiere de otras clases y puede que necesitemos diferentes combinaciones. Aquí es cuando nuestro Object Mother empezaría a crecer, se empiezan a añadir métodos y se vuelve un caos. Para solucionar esto podríamos utilizar Test Data Builder que nos va a proporcionar más flexibilidad, más orden y seguramente mejoremos la legibilidad. En el ejemplo del empleado que tenemos ahora mismo quizá no se vean tan claras las ventajas, pero se vería más o menos así:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class EmployeeBuilder { private readonly Employee employee; public EmployeeBuilder() { employee = new Employee("María", 23); } public EmployeeBuilder Underage() { employee.Age = 16; return this; } public static implicit operator Employee(EmployeeBuilder builder) { return builder.employee; } } |
No es el mejor ejemplo, pero creo que se ve la intención. Seguramente dentro de nuestro EmployeeBuilder utilizaríamos otros builders de otras clases y nos va a permitir ocultar los detalles que no son necesarios para la creación de nuestro Employee y haría que perdiésemos el foco de lo que necesita el test. Ahora nuestro test sería así (utilizando un empleado menor de edad):
1 2 3 4 5 6 7 8 9 10 11 | [Fact] public void AddEmployeeToDepartment() { Employee employee = new EmployeeBuilder().Underage(); Department department = new Department(); department.AddEmployee(employee); var result = department.ContainsEmployee(employee); Assert.True(result); } |
Por último, otra opción es tener métodos de creación dentro de la propia clase de test. Si por ejemplo queremos crear un empleado que tenga permiso remunerado al acudir a un examen, sólo eso, nos da igual que para poder crear un empleado con esas características necesitemos crear un convenio colectivo con ciertas especificaciones, algunas de ellas irrelevantes pero necesarias para construir el objeto o que tenga que tener 3 años de antigüedad, puede que sea una buena opción crear el método en la propia clase de test y seguramente ese método interactúe con data builders y object mothers de creación.
1 2 3 4 5 6 7 | private Employee GivenAEmployeeWithPaidTimeOffForExam() { //Aquí utilizaríamos todos builders, Object Mother y news necesarios //para crear el empleado con las características que queremos return new EmployeeBuilder() //.....// //.......// } |
Y todo este rollo que has leído mil veces qué tiene que ver con Autofixture, pues que su objetivo precisamente es el mismo que las técnicas que hemos visto, pero nos ayuda a escribir menos código para conseguirlo.
Vamos a ver unos ejemplos rápidos de Autofixture:
1 2 3 4 5 6 7 | Fixture fixture = new Fixture(); string text = fixture.Create<string>();//fadacf70-0d0e-4bcc-9647-e7fef54849327 int number = fixture.Create<int>();//133 IEnumerable<string> textList = fixture.CreateMany<string>(); //27b8b082-f53f-409a-90b7-3dcd3c77bc45 //2c51a025-f526-4434-ba68-c73f29e03477 //a22809d6-69b0-4f2a-91dd-9686e37b61f2 |
Vemos que necesitamos crear una instancia de Fixture y le podemos solicitar que nos genere valores del tipo que queramos con Create
1 2 3 4 | Fixture fixture = new Fixture(); Employee employee = fixture.Create<Employee>(); //employee.Age = 131 //employee.Name = Name15a5ea9c-8c6d-41cb-9e01-e443ae5cbb9d |
Pues como era de esperar, funciona correctamente para tipos más complejos. No sólo genera valores para los datos que requiere el constructor, sino que cualquier propiedad pública va a ser asignada con valores. Si a nuestra clase Employee le añadiésemos una propiedad Surname, ésta también sería asignada con datos. Incluso también funciona con interfaces del framework como IEnumerable
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class Hobby { public string Name { get; set; } } public class Employee { public string Name { get; set; } public int Age { get; set; } public string Surname { get; set; } public IEnumerable<Hobby> Hobbies { get; set; } public Employee(string name, int age) { Name = name; Age = age; } } |
Nos va a crear un employee con datos para todas las propiedades públicas:
1 2 3 4 5 6 7 8 | Fixture fixture = new Fixture(); Employee employee = fixture.Create<Employee>(); //employee.Age = 131 //employee.Name = Name15a5ea9c-8c6d-41cb-9e01-e443ae5cbb9d //employee.Surname = Surnameaac04cde-00a7-452e-a8c4-92828997f6cd //employee.Hobbies[0] = Name80775967-2c3e-4be8-9100-47ea5c5345c1 //employee.Hobbies[1] = Name2d8600b4-9f03-4a5b-948d-e8948ca2a0fb //employee.Hobbies[1] = Name3745fe4e-4bd6-4f68-a5b5-5f9572c48fb3 |
A partir de aquí nos ofrece un montón de configuraciones para ajustar la creación de nuestros objetos a nuestras necesidades. Por ejemplo, podemos hacer que las autoimplementadas no se rellenen con datos:
1 2 3 4 5 6 7 8 9 10 11 | Fixture fixture = new Fixture { OmitAutoProperties = true }; Employee employee = fixture.Create<Employee>(); //employee.Age = 131 //employee.Name = Name15a5ea9c-8c6d-41cb-9e01-e443ae5cbb9d //employee.Surname = null //employee.Hobbies = null //Otra opción sería así Fixture fixture = new Fixture(); Employee employee = fixture.Build<Employee>(). OmitAutoProperties().Create(); |
Podemos omitir que ciertas propiedades tengan valor:
1 2 3 4 5 | Fixture fixture = new Fixture(); Employee employee = fixture.Build<Employee>(). Without((e) => e.Surname).Create(); //employee.Surname = null |
Hay un montón más de opciones que podemos configurar. Como estamos viendo, Autofixture nos está ayudando a generar un Test Data Builder de forma automática. Si por ejemplo el empleado además de tener una lista de aficiones, tiene una afición favorita dentro de esa lista de aficiones, lo podríamos construir así:
1 2 3 4 5 6 | Fixture fixture = new Fixture(); Hobby hobby = fixture.Create<Hobby>(); Employee employee = fixture.Build<Employee>(). With(e=>e.Hobbies, new List<Hobby>() { hobby }). With(e => e.BiggestHobby, hobby). Create(); |
También podemos ejecutar acciones sobre nuestro empleado, pero ojo, se van a ejecutar antes de la asignación de propiedades, por lo que para hacer lo mismo que en el ejemplo anterior deberíamos omitir la creación de las propiedades. Vamos a asumir que en el constructor del empleado se hace un new List
1 2 3 4 5 6 7 | Fixture fixture = new Fixture(); Hobby hobby = fixture.Create<Hobby>(); Employee employee = fixture.Build<Employee>(). Do(e => e.Hobbies.Add(hobby)). Without(e => e.Hobbies). With(e=>e.BiggestHobby,hobby) .Create(); |
Estos dos últimos ejemplos se ven más claros dentro del contexto de creación de una view model donde tengamos una lista de algo y un elemento seleccionado de esa lista.
Repasando un poco, hemos conseguido principalmente que la creación de los objetos que necesitamos pero no son importantes (o sus datos no lo son) para el test, ahora es mucho más rápida y no estamos acoplados a sus detalles. Además de que, esos datos que necesitamos y ponemos valores que el que lee el test no tiene claro si ese valor es relevante o simplemente se puso uno al azar, ahora está seguro de que el valor del dato no es relevante. Además nos proporciona una forma de crear builders que, aunque no sean tan expresivos como si los creamos nosotros, son mucho menos costosos de hacer.
¿Y qué hay de la inyección de dependencias? Aquí típicamente vamos a pasar por constructor ciertas interfaces y claro, necesitan una implementación… La primera solución que tenemos es registrar una función para crear un tipo que cumpla la interfaz. Como vimos antes, para la interfaz IEnumerable
1 2 3 4 5 | Fixture fixture = new Fixture(); fixture.Register<IEnumerable<string>>(() => new List<string>(){"María", "Pepe"}); var enumerableString = fixture.Create<IEnumerable<string>>(); //enumerableString[0] = "María" //enumerableString[1] = "Pepe" |
Ahora vamos a pensar que tenemos un sistema para imprimir los nombres de los empleados… sí, no se me ha ocurrido nada mejor 🙂
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 | public interface IPrinterEmployeesService { void Print(); } public class PrinterEmployeesService : IPrinterEmployeesService { private readonly IRepository<Employee> employeeRepository; private readonly IPrinter printer; public PrinterEmployeesService(IRepository<Employee> employeeRepository, IPrinter printer) { this.employeeRepository = employeeRepository; this.printer = printer; } public void Print() { var employees = employeeRepository.GetAll(); foreach (var employee in employees) { printer.Print(employee.Name); } } } |
Si quisiéramos hacer un test que confirme que se utiliza al menos una vez el método GetAll de employeeRepository, lo podríamos hacer así con Autofixture:
1 2 3 4 5 6 7 8 9 10 11 | [Fact] public void PrinterEmployeesUseRepository() { IFixture fixture = new Fixture().Customize(new AutoMoqCustomization()); var repository = fixture.Freeze<Mock<IRepository<Employee>>>(); var printerEmployeesService = fixture.Create<PrinterEmployeesService>(); printerEmployeesService.Print(); repository.Verify(r => r.GetAll(), Times.AtLeastOnce); } |
Vamos por partes. En la primera línea estamos creando un nuevo Fixture y le añadimos AutoMoqCustomization. Con esto lo que conseguimos es que, cuando se cree PrinterEmployeesService o en general cualquier cosa que tenga una interfaz o clase abstracta que no pueda resolver, va a añadir un Mock
¿Y si ahora queremos hacer un test en el que se compruebe que se imprime el nombre de los empleados? De nuevo estamos en la situación en la que no nos interesa cómo se crean los empleados, sólo queremos que nuestro repositorio devuelva empleados:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | [Fact] public void PrinterPrintsEmployeeName() { IFixture fixture = new Fixture().Customize(new AutoMoqCustomization()); var employees = fixture.CreateMany<Employee>().ToList(); fixture.Freeze<Mock<IRepository<Employee>>>().Setup(r => r.GetAll()).Returns(employees); var printer = fixture.Freeze<Mock<IPrinter>>(); var printerEmployeesService = fixture.Create<PrinterEmployeesService>(); printerEmployeesService.Print(); printer.Verify(p => p.Print(It.Is<string>(n=>employees.Select(e=>e.Name).Contains(n))), Times.Exactly(employees.Count)); } |
En este test volvemos a estar desacoplados de la creación de nuestros objetos. Si se añaden nuevos parámetros al constructor de Employee, nos da igual, si se añaden a PrinterEmployeesService, seguramente también. Este test tiene algún problema, entre otras cosas que si employees no contiene ningún elemento, el test pasa (aunque a lo mejor debería ser así), y tampoco está verificando que se utilice cada uno de los nombres de los empleados una sola vez, es decir, si se imprimiese todo el rato el nombre del primer empleado, el test pasaría igualmente. La última línea la podríamos cambiar por lo que vemos a continuación:
1 2 3 4 | //printer.Verify(p => p.Print(It.Is<string>(n=>employees.Select(e=>e.Name).Contains(n))), // Times.Exactly(employees.Count)); foreach (var employee in employees) printer.Verify(p => p.Print(employee.Name), Times.Once); |
El test quedaría más feo pero hemos cubierto el caso de que se imprima el nombre de todos los empleados, aunque ahora ya no estamos asegurando el número de veces que se llama a p.Print 🙂 Aquí el problema no es Autofixture, es mío, que por desgracia llevo demasiado tiempo sin hacer test (por eso escribo estos post, para no oxidarme), así que pido disculpas.
Por último vamos a ver cómo combinar Autofixture con Theory (de xUnit) y poder pasar directamente los datos como parámetros del test:
1 2 3 4 5 6 7 8 9 10 | [Theory, AutoData] public void AddEmployeeToDepartment(Employee employee) { Department department = new Department(); department.AddEmployee(employee); var result = department.ContainsEmployee(employee); Assert.True(result); } |
El empleado se va a crear directamente gracias a AutoData. También podemos hacer algo similar a AutoMoqCustomization con AutoMoqData. Vemos cómo quedaría el primer test en el que usábamos AutoMoqCustomization:
1 2 3 4 5 6 7 8 | [Theory, AutoMoqData] public void PrinterEmployeesUseRepository(PrinterEmployeesService printerEmployeesService, [Frozen]Mock<IRepository<Employee>> repository) { printerEmployeesService.Print(); repository.Verify(r => r.GetAll(), Times.AtLeastOnce); } |
Donde AutoMoqData es:
1 2 3 4 5 6 7 | public class AutoMoqDataAttribute : AutoDataAttribute { public AutoMoqDataAttribute() : base(() => new Fixture().Customize(new AutoMoqCustomization())) { } } |
Un último caso de uso de Autofixture podría ser generar muchos valores para comprobar ciertos algoritmos en los que, aunque no sepamos al 100% que el algoritmo es el correcto, sí tengamos un grado de confianza muy alto.
Conclusiones
Después de ver por encima qué podemos hacer con Autofixture, parece una herramienta bastante potente y flexible principalmente para ahorrarnos código. Un efecto colateral de utilizarla es mantenernos desacoplados de los constructores en nuestros test y hacer que las refactorizaciones de código y las nuevas funcionalidades que añadamos no hagan romper cientos de test, pero esto lo deberíamos intentar conseguir usemos o no Autofixture. Quedan muchas cosas muy interesantes por ver de Autofixture, pero como introducción está bien 🙂 Os dejo un recurso para ver casi todo lo que podemos hacer con Autofixture.
¡Un saludo!
Gran post.
Has cubierto todo lo que se me ocurre usar con Autofixture.
Ayuda a difundir conocimiento en Español que hay muy poco.
Gracias.