sql >> Database teknologi >  >> RDS >> Database

Hvorfor brug af enhedstests er en stor investering i højkvalitetsarkitektur

Jeg har besluttet at skrive denne artikel for at vise, at enhedstests ikke kun er et værktøj til at kæmpe med regression i koden, men også er en stor investering i en arkitektur af høj kvalitet. Derudover motiverede et emne i det engelske .NET-fællesskab mig til at gøre dette. Forfatteren til artiklen var Johnnie. Han beskrev sin første og sidste dag i virksomheden involveret i softwareudvikling til erhvervslivet i den finansielle sektor. Johnnie søgte stillingen – som udvikler af enhedstests. Han var ked af den dårlige kodekvalitet, som han skulle teste. Han sammenlignede koden med en skraldeplads fyldt med genstande, der kloner hinanden på uegnede steder. Derudover kunne han ikke finde abstrakte datatyper i et lager:koden indeholdt kun binding af implementeringer, der krydsanmoder hinanden.

Johnnie, der indså al nytteløsheden af ​​modultestning i denne virksomhed, skitserede denne situation for lederen, nægtede yderligere samarbejde og gav et værdifuldt råd. Han anbefalede, at et udviklingsteam gik på kurser for at lære at instansiere objekter og bruge abstrakte datatyper. Jeg ved ikke, om lederen fulgte hans råd (det tror jeg ikke, han gjorde). Men hvis du er interesseret i, hvad Johnnie mente, og hvordan brug af modultestning kan påvirke kvaliteten af ​​din arkitektur, er du velkommen til at læse denne artikel.

Afhængighedsisolering er en base for modultestning

Modul- eller enhedstest er en test, der verificerer modulets funktionalitet isoleret fra dets afhængigheder. Afhængighedsisolation er en substitution af objekter fra den virkelige verden, som modulet, der testes, interagerer med, med stubbe, der simulerer den korrekte adfærd af deres prototyper. Denne substitution gør det muligt at fokusere på at teste et bestemt modul og ignorere en mulig forkert adfærd i dets omgivelser. En nødvendighed for at erstatte afhængigheder i testen forårsager en interessant egenskab. En udvikler, der indser, at deres kode vil blive brugt i modultest, skal udvikle ved hjælp af abstraktioner og udføre refactoring ved de første tegn på høj forbindelse.

Jeg vil overveje det i det konkrete eksempel.

Lad os prøve at forestille os, hvordan et personligt beskedmodul kan se ud på et system udviklet af det firma, som Johnnie flygtede fra. Og hvordan det samme modul ville se ud, hvis udviklere skulle anvende enhedstest.

Modulet skal være i stand til at gemme beskeden i databasen, og hvis den person, som beskeden var adresseret til, er i systemet — vis beskeden på skærmen med en toast-notifikation.

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

Lad os tjekke, hvilke afhængigheder vores modul har.

SendMessage-funktionen påkalder statiske metoder for Notificationsservice- og Usersservice-objekterne og opretter Messagesrepository-objektet, der er ansvarligt for at arbejde med databasen.

Der er ingen problemer med, at modulet interagerer med andre objekter. Problemet er, hvordan denne interaktion er bygget, og den er ikke bygget med succes. Direkte adgang til tredjepartsmetoder har gjort vores modul tæt knyttet til specifikke implementeringer.

Denne interaktion har mange ulemper, men det vigtige er, at Messagingservice-modulet har mistet muligheden for at blive testet isoleret fra implementeringerne af Notificationsservice, Usersservice og Messagesrepository. Faktisk kan vi ikke erstatte disse objekter med stubbe.

Lad os nu se på, hvordan det samme modul ville se ud, hvis en udvikler skulle tage sig af det.

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

Som du kan se, er denne version meget bedre. Interaktionen mellem objekter bygges nu ikke direkte, men gennem grænseflader.

Vi behøver ikke længere at få adgang til statiske klasser og instansiere objekter i metoder med forretningslogik. Hovedpointen er, at vi kan erstatte alle afhængigheder ved at overføre stubs til test i en konstruktør. Mens vi forbedrer kodetestbarheden, kunne vi således også forbedre både testbarheden af ​​vores kode og arkitekturen af ​​vores applikation. Vi nægtede at bruge implementeringer direkte og sendte instansiering til laget ovenfor. Det er præcis, hvad Johnnie ønskede.

Opret derefter en test for modulet til at sende beskeder.

Specifikation om test

Definer, hvad vores test skal kontrollere:

  • Et enkelt kald af SaveMessage-metoden
  • Et enkelt kald af SendNotificationToUser()-metoden, hvis IsUserOnline()-metodestubben over IUsersService-objektet returnerer true
  • Der er ingen SendNotificationToUser()-metode, hvis IsUserOnline()-metodestubben over IUsersService-objektet returnerer false

At følge disse betingelser kan garantere, at implementeringen af ​​SendMessage-meddelelsen er korrekt og ikke indeholder nogen fejl.

Tests

Testen er implementeret ved hjælp af den isolerede Moq-ramme

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

For at opsummere er det en ubrugelig opgave at lede efter en ideel arkitektur.

Enhedstests er gode at bruge, når du skal tjekke arkitekturen ved tab af kobling mellem moduler. Alligevel skal du huske på, at design af komplekse tekniske systemer altid er et kompromis. Der er ingen ideel arkitektur, og det er ikke muligt at tage højde for alle scenarierne i applikationsudviklingen på forhånd. Arkitekturens kvalitet afhænger af flere parametre, som ofte udelukker hinanden. Du kan løse ethvert designproblem ved at tilføje et ekstra abstraktionsniveau. Det refererer dog ikke til problemet med en enorm mængde abstraktionsniveauer. Jeg anbefaler ikke at tro, at interaktion mellem objekter kun er baseret på abstraktioner. Pointen er, at du bruger koden, der tillader interaktion mellem implementeringer og er mindre fleksibel, hvilket betyder, at den ikke har mulighed for at blive testet ved enhedstests.


  1. Tjek, om et Postgres JSON-array indeholder en streng

  2. ORA-00936:manglende udtryksorakel

  3. Udbyderen er ikke kompatibel med versionen af ​​Oracle-klienten

  4. Sådan beregnes forskellen mellem to tidsstempler i MySQL