Database
 sql >> Base de données >  >> RDS >> Database

Pourquoi l'utilisation des tests unitaires est un excellent investissement dans une architecture de haute qualité

J'ai décidé d'écrire cet article afin de montrer que les tests unitaires ne sont pas seulement un outil pour lutter contre la régression dans le code, mais aussi un excellent investissement dans une architecture de haute qualité. De plus, un sujet dans la communauté .NET anglaise m'a motivé à le faire. L'auteur de l'article était Johnnie. Il a décrit son premier et dernier jour dans l'entreprise impliquée dans le développement de logiciels pour les entreprises du secteur financier. Johnnie postulait pour le poste – de développeur de tests unitaires. Il était contrarié par la mauvaise qualité du code, qu'il devait tester. Il a comparé le code à un dépotoir bourré d'objets qui se clonent dans tous les endroits inappropriés. De plus, il n'a pas pu trouver de types de données abstraits dans un référentiel :le code ne contenait que la liaison des implémentations qui se requéraient mutuellement.

Johnnie, réalisant toute l'inutilité des tests de modules dans cette entreprise, a exposé cette situation au responsable, a refusé de continuer à coopérer et a donné un conseil précieux. Il a recommandé qu'une équipe de développement suive des cours pour apprendre à instancier des objets et à utiliser des types de données abstraits. Je ne sais pas si le responsable a suivi ses conseils (je pense qu'il ne l'a pas fait). Cependant, si vous êtes intéressé par ce que Johnnie voulait dire et comment l'utilisation des tests de modules peut influencer la qualité de votre architecture, vous êtes invités à lire cet article.

L'isolation des dépendances est une base de test de module

Le test de module ou d'unité est un test qui vérifie la fonctionnalité du module isolé de ses dépendances. L'isolation des dépendances est une substitution d'objets du monde réel, avec lesquels le module testé interagit, avec des stubs qui simulent le comportement correct de leurs prototypes. Cette substitution permet de se concentrer sur le test d'un module particulier, en ignorant un éventuel comportement incorrect de son environnement. Une nécessité de remplacer les dépendances dans le test provoque une propriété intéressante. Un développeur qui se rend compte que son code sera utilisé dans des tests de modules doit développer en utilisant des abstractions et effectuer une refactorisation dès les premiers signes d'une connectivité élevée.

Je vais l'examiner sur l'exemple particulier.

Essayons d'imaginer à quoi pourrait ressembler un module de message personnel sur un système développé par l'entreprise dont Johnnie s'est échappé. Et à quoi ressemblerait le même module si les développeurs appliquaient les tests unitaires.

Le module doit être capable de stocker le message dans la base de données et si la personne à qui le message a été adressé est dans le système — afficher le message à l'écran avec une notification toast.

//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);
        }
    }
}

Vérifions les dépendances de notre module.

La fonction SendMessage appelle les méthodes statiques des objets Notificationsservice et Usersservice et crée l'objet Messagesrepository responsable de l'utilisation de la base de données.

Il n'y a aucun problème avec le fait que le module interagit avec d'autres objets. Le problème est de savoir comment cette interaction est construite, et elle n'est pas construite avec succès. L'accès direct à des méthodes tierces a rendu notre module étroitement lié à des implémentations spécifiques.

Cette interaction a beaucoup d'inconvénients, mais l'important est que le module Messagingservice a perdu la possibilité d'être testé indépendamment des implémentations du Notificationsservice, Usersservice et Messagesrepository. En fait, nous ne pouvons pas remplacer ces objets par des stubs.

Voyons maintenant à quoi ressemblerait le même module si un développeur s'en occupait.

//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);
        }
    }
}

Comme vous pouvez le voir, cette version est bien meilleure. L'interaction entre les objets ne se construit plus directement mais via des interfaces.

Nous n'avons plus besoin d'accéder à des classes statiques et d'instancier des objets dans des méthodes avec une logique métier. Le point principal est que nous pouvons remplacer toutes les dépendances en passant des stubs à tester dans un constructeur. Ainsi, tout en améliorant la testabilité du code, nous pourrions également améliorer à la fois la testabilité de notre code et l'architecture de notre application. Nous avons refusé d'utiliser directement les implémentations et avons passé l'instanciation à la couche supérieure. C'est exactement ce que voulait Johnnie.

Ensuite, créez un test pour le module d'envoi de messages.

Spécification sur les tests

Définissez ce que notre test doit vérifier :

  • Un seul appel de la méthode SaveMessage
  • Un seul appel de la méthode SendNotificationToUser() si le stub de la méthode IsUserOnline() sur l'objet IUsersService renvoie true
  • Il n'y a pas de méthode SendNotificationToUser() si le stub de la méthode IsUserOnline() sur l'objet IUsersService renvoie false

Le respect de ces conditions peut garantir que la mise en œuvre du message SendMessage est correcte et ne contient aucune erreur.

Tests

Le test est implémenté en utilisant le framework Moq isolé

[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);
}

Pour résumer, chercher une architecture idéale est une tâche inutile.

Les tests unitaires sont parfaits à utiliser lorsque vous devez vérifier l'architecture sur la perte de couplage entre les modules. Cependant, gardez à l'esprit que la conception de systèmes d'ingénierie complexes est toujours un compromis. Il n'y a pas d'architecture idéale et il n'est pas possible de prendre en compte en amont tous les scénarios de développement de l'application. La qualité de l'architecture dépend de plusieurs paramètres, souvent mutuellement exclusifs. Vous pouvez résoudre n'importe quel problème de conception en ajoutant un niveau d'abstraction supplémentaire. Cependant, cela ne fait pas référence au problème d'une quantité énorme de niveaux d'abstraction. Je déconseille de penser que l'interaction entre objets ne repose que sur des abstractions. Le fait est que vous utilisez le code qui permet l'interaction entre les implémentations et qui est moins flexible, ce qui signifie qu'il n'a pas la possibilité d'être testé par des tests unitaires.