Decidí escribir este artículo para mostrar que las pruebas unitarias no solo son una herramienta para lidiar con la regresión en el código, sino que también son una gran inversión en una arquitectura de alta calidad. Además, un tema en la comunidad de .NET en inglés me motivó a hacer esto. El autor del artículo fue Johnnie. Describió su primer y último día en la empresa dedicada al desarrollo de software para empresas del sector financiero. Johnnie estaba solicitando el puesto de desarrollador de pruebas unitarias. Estaba molesto con la mala calidad del código, que tenía que probar. Comparó el código con un depósito de chatarra repleto de objetos que se clonan entre sí en cualquier lugar inadecuado. Además, no pudo encontrar tipos de datos abstractos en un repositorio:el código contenía solo enlaces de implementaciones que se solicitan entre sí.
Johnnie, al darse cuenta de toda la inutilidad de las pruebas de módulos en esta empresa, le explicó esta situación al gerente, se negó a seguir cooperando y le dio un valioso consejo. Recomendó que un equipo de desarrollo asistiera a cursos para aprender a crear instancias de objetos y usar tipos de datos abstractos. No sé si el gerente siguió su consejo (creo que no). Sin embargo, si está interesado en lo que Johnnie quiso decir y cómo el uso de pruebas de módulos puede influir en la calidad de su arquitectura, le invitamos a leer este artículo.
El aislamiento de dependencia es la base de las pruebas de módulos
La prueba de módulo o unidad es una prueba que verifica la funcionalidad del módulo aislada de sus dependencias. El aislamiento de dependencia es una sustitución de los objetos del mundo real, con los que interactúa el módulo que se está probando, con stubs que simulan el comportamiento correcto de sus prototipos. Esta sustitución permite centrarse en probar un módulo en particular, ignorando un posible comportamiento incorrecto de su entorno. La necesidad de reemplazar las dependencias en la prueba genera una propiedad interesante. Un desarrollador que se da cuenta de que su código se usará en las pruebas del módulo tiene que desarrollar usando abstracciones y realizar refactorizaciones ante los primeros signos de alta conectividad.
Lo voy a considerar en el ejemplo particular.
Intentemos imaginar cómo sería un módulo de mensajes personales en un sistema desarrollado por la empresa de la que escapó Johnnie. Y cómo se vería el mismo módulo si los desarrolladores aplicaran pruebas unitarias.
El módulo debería poder almacenar el mensaje en la base de datos y, si la persona a la que se envió el mensaje está en el sistema, mostrar el mensaje en la pantalla con una notificación de brindis.
//A module for sending messages in C#. Version 1. public class MessagingService { public void SendMessage(Guid messageAuthorId, Guid messageRecieverId, string message) { //A repository object stores a message in a database new MessagesRepository().SaveMessage(messageAuthorId, messageRecieverId, message); //check if the user is online if (UsersService.IsUserOnline(messageRecieverId)) { //send a toast notification calling the method of a static object NotificationsService.SendNotificationToUser(messageAuthorId, messageRecieverId, message); } } }
Veamos qué dependencias tiene nuestro módulo.
La función SendMessage invoca métodos estáticos de los objetos Notificationsservice y Usersservice y crea el objeto Messagesrepository que es responsable de trabajar con la base de datos.
No hay problema con el hecho de que el módulo interactúe con otros objetos. El problema es cómo se construye esta interacción, y no se construye con éxito. El acceso directo a métodos de terceros ha hecho que nuestro módulo esté estrechamente vinculado a implementaciones específicas.
Esta interacción tiene muchas desventajas, pero lo importante es que el módulo Messagingservice ha perdido la capacidad de probarse de forma aislada de las implementaciones de Notificationsservice, Usersservice y Messagesrepository. En realidad, no podemos reemplazar estos objetos con stubs.
Ahora veamos cómo se vería el mismo módulo si un desarrollador se encargara de él.
//A module for sending messages in C#. Version 2. public class MessagingService: IMessagingService { private readonly IUserService _userService; private readonly INotificationService _notificationService; private readonly IMessagesRepository _messagesRepository; public MessagingService(IUserService userService, INotificationService notificationService, IMessagesRepository messagesRepository) { _userService = userService; _notificationService = notificationService; _messagesRepository = messagesRepository; } public void AddMessage(Guid messageAuthorId, Guid messageRecieverId, string message) { //A repository object stores a message in a database. _messagesRepository.SaveMessage(messageAuthorId, messageRecieverId, message); //check if the user is online if (_userService.IsUserOnline(messageRecieverId)) { //send a toast message _notificationService.SendNotificationToUser(messageAuthorId, messageRecieverId, message); } } }
Como puedes ver, esta versión es mucho mejor. La interacción entre objetos ahora no se construye directamente sino a través de interfaces.
Ya no necesitamos acceder a clases estáticas e instanciar objetos en métodos con lógica empresarial. El punto principal es que podemos reemplazar todas las dependencias pasando stubs para probar en un constructor. Por lo tanto, mientras mejoramos la capacidad de prueba del código, también podríamos mejorar tanto la capacidad de prueba de nuestro código como la arquitectura de nuestra aplicación. Nos negamos a usar implementaciones directas y pasamos la creación de instancias a la capa superior. Esto es exactamente lo que Johnnie quería.
A continuación, cree una prueba para el módulo de envío de mensajes.
Especificación de las pruebas
Defina lo que nuestra prueba debe comprobar:
- Una sola llamada del método SaveMessage
- Una sola llamada del método SendNotificationToUser() si el código auxiliar del método IsUserOnline() sobre el objeto IUsersService devuelve verdadero
- No hay método SendNotificationToUser() si el código auxiliar del método IsUserOnline() sobre el objeto IUsersService devuelve falso
Seguir estas condiciones puede garantizar que la implementación del mensaje SendMessage sea correcta y no contenga ningún error.
Pruebas
La prueba se implementa utilizando el marco Moq aislado
[TestMethod] public void AddMessage_MessageAdded_SavedOnce() { //Arrange //sender Guid messageAuthorId = Guid.NewGuid(); //receiver who is online Guid recieverId = Guid.NewGuid(); //a message sent from a sender to a receiver string msg = "message"; // stub for the IsUserOnline interface of the IUserService method Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior()); userServiceStub.Setup(x => x.IsUserOnline(It.IsAny<Guid>())).Returns(true); //mocks for INotificationService and IMessagesRepository Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>(); Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>(); //create a module for messages passing mocks and stubs as dependencies var messagingService = new MessagingService(userServiceStub.Object, notificationsServiceMoq.Object, repositoryMoq.Object); //Act messagingService.AddMessage(messageAuthorId, recieverId, msg); //Assert repositoryMoq.Verify(x => x.SaveMessage(messageAuthorId, recieverId, msg), Times.Once); } [TestMethod] public void AddMessage_MessageSendedToOffnlineUser_NotificationDoesntRecieved() { //Arrange //sender Guid messageAuthorId = Guid.NewGuid(); //receiver who is offline Guid offlineReciever = Guid.NewGuid(); //message sent from a sender to a receiver string msg = "message"; // stub for the IsUserOnline interface of the IUserService method Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior()); userServiceStub.Setup(x => x.IsUserOnline(offlineReciever)).Returns(false); //mocks for INotificationService and IMessagesRepository Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>(); Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>(); // create a module for messages passing mocks and stubs as dependencies var messagingService = new MessagingService(userServiceStub.Object, notificationsServiceMoq.Object, repositoryMoq.Object); //Act messagingService.AddMessage(messageAuthorId, offlineReciever, msg); //Assert notificationsServiceMoq.Verify(x => x.SendNotificationToUser(messageAuthorId, offlineReciever, msg), Times.Never); } [TestMethod] public void AddMessage_MessageSendedToOnlineUser_NotificationRecieved() { //Arrange //sender Guid messageAuthorId = Guid.NewGuid(); //receiver who is online Guid onlineRecieverId = Guid.NewGuid(); //message sent from a sender to a receiver string msg = "message"; // stub for the IsUserOnline interface of the IUserService method Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior()); userServiceStub.Setup(x => x.IsUserOnline(onlineRecieverId)).Returns(true); //mocks for INotificationService and IMessagesRepository Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>(); Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>(); //create a module for messages passing mocks and stubs as dependencies var messagingService = new MessagingService(userServiceStub.Object, notificationsServiceMoq.Object, repositoryMoq.Object); //Act messagingService.AddMessage(messageAuthorId, onlineRecieverId, msg); //Assert notificationsServiceMoq.Verify(x => x.SendNotificationToUser(messageAuthorId, onlineRecieverId, msg), Times.Once); }
En resumen, buscar una arquitectura ideal es una tarea inútil.
Las pruebas unitarias son excelentes para usar cuando necesita verificar la arquitectura en caso de pérdida de acoplamiento entre módulos. Aún así, tenga en cuenta que el diseño de sistemas de ingeniería complejos es siempre un compromiso. No existe una arquitectura ideal y no es posible tener en cuenta todos los escenarios del desarrollo de la aplicación de antemano. La calidad de la arquitectura depende de múltiples parámetros, a menudo mutuamente excluyentes. Puede resolver cualquier problema de diseño agregando un nivel adicional de abstracción. Sin embargo, no se refiere al problema de una gran cantidad de niveles de abstracción. No recomiendo pensar que la interacción entre objetos se basa únicamente en abstracciones. El punto es que usas el código que permite la interacción entre implementaciones y es menos flexible, lo que significa que no tiene la posibilidad de ser probado por pruebas unitarias.