sql >> Base de Datos >  >> RDS >> Database

Por qué el uso de pruebas unitarias es una gran inversión en arquitectura de alta calidad

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.