sql >> Databasteknik >  >> RDS >> Database

Varför att använda enhetstester är en stor investering i högkvalitativ arkitektur

Jag har bestämt mig för att skriva den här artikeln för att visa att enhetstester inte bara är ett verktyg för att brottas med regression i koden utan också är en stor investering i en högkvalitativ arkitektur. Dessutom motiverade ett ämne i det engelska .NET-communityt mig att göra detta. Författaren till artikeln var Johnnie. Han beskrev sin första och sista dag i företaget som är involverat i mjukvaruutveckling för företag inom finanssektorn. Johnnie sökte tjänsten – som utvecklare av enhetstester. Han var upprörd över den dåliga kodkvaliteten, som han var tvungen att testa. Han jämförde koden med en skrotupplag fylld med föremål som klonar varandra på alla olämpliga ställen. Dessutom kunde han inte hitta abstrakta datatyper i ett arkiv:koden innehöll bara bindning av implementeringar som korsar begär varandra.

Johnnie insåg all värdelöshet med modultestning i det här företaget beskrev denna situation för chefen, vägrade från ytterligare samarbete och gav ett värdefullt råd. Han rekommenderade att ett utvecklingsteam skulle gå på kurser för att lära sig instansiera objekt och använda abstrakta datatyper. Jag vet inte om chefen följde hans råd (jag tror att han inte gjorde det). Men om du är intresserad av vad Johnnie menade och hur användning av modultestning kan påverka kvaliteten på din arkitektur, är du välkommen att läsa den här artikeln.

Beroendeisolering är en bas för modultestning

Modul- eller enhetstest är ett test som verifierar modulens funktionalitet isolerad från dess beroenden. Beroendeisolering är en ersättning av verkliga objekt, som modulen som testas interagerar med, med stubbar som simulerar det korrekta beteendet hos deras prototyper. Denna ersättning gör det möjligt att fokusera på att testa en viss modul, och ignorera ett eventuellt felaktigt beteende i dess miljö. En nödvändighet att ersätta beroenden i testet orsakar en intressant egenskap. En utvecklare som inser att deras kod kommer att användas i modultester måste utveckla med hjälp av abstraktioner och utföra refactoring vid de första tecknen på hög anslutning.

Jag kommer att överväga det på det specifika exemplet.

Låt oss försöka föreställa oss hur en personlig meddelandemodul kan se ut på ett system utvecklat av företaget som Johnnie flydde från. Och hur samma modul skulle se ut om utvecklare skulle tillämpa enhetstestning.

Modulen ska kunna lagra meddelandet i databasen och om personen som meddelandet var adresserat till finns i systemet — visa meddelandet på skärmen med en skålnotis.

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

Låt oss kontrollera vilka beroenden vår modul har.

SendMessage-funktionen anropar statiska metoder för Notificationsservice- och Usersservice-objekten och skapar Messagesrepository-objektet som ansvarar för att arbeta med databasen.

Det är inga problem med att modulen interagerar med andra objekt. Problemet är hur den här interaktionen är uppbyggd, och den har inte byggts framgångsrikt. Direkt åtkomst till tredjepartsmetoder har gjort vår modul tätt kopplad till specifika implementeringar.

Denna interaktion har många nackdelar, men det viktiga är att Messagingservice-modulen har förlorat förmågan att testas isolerat från implementeringarna av Notificationsservice, Usersservice och Messagesrepository. Vi kan faktiskt inte ersätta dessa objekt med stubbar.

Låt oss nu titta på hur samma modul skulle se ut om en utvecklare skulle ta hand om den.

//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 är den här versionen mycket bättre. Interaktionen mellan objekt byggs nu inte direkt utan genom gränssnitt.

Vi behöver inte längre komma åt statiska klasser och instansiera objekt i metoder med affärslogik. Huvudpoängen är att vi kan ersätta alla beroenden genom att skicka in stubbar för testning till en konstruktor. Således, samtidigt som vi förbättrar kodtestbarheten, kan vi också förbättra både testbarheten för vår kod och arkitekturen för vår applikation. Vi vägrade direkt använda implementeringar och skickade instansieringen till lagret ovan. Det här är precis vad Johnnie ville ha.

Skapa sedan ett test för modulen för att skicka meddelanden.

Specifikation om tester

Definiera vad vårt test ska kontrollera:

  • Ett enda anrop av metoden SaveMessage
  • Ett enstaka anrop av metoden SendNotificationToUser() om metodstubben IsUserOnline() över IUsersService-objektet returnerar true
  • Det finns ingen SendNotificationToUser()-metod om metodstubben IsUserOnline() över IUsersService-objektet returnerar false

Att följa dessa villkor kan garantera att implementeringen av SendMessage-meddelandet är korrekt och inte innehåller några fel.

Tester

Testet implementeras med hjälp av det isolerade Moq-ramverket

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

För att sammanfatta det är att leta efter en idealisk arkitektur en värdelös uppgift.

Enhetstester är bra att använda när du behöver kontrollera arkitekturen vid förlorad koppling mellan moduler. Kom ändå ihåg att design av komplexa tekniska system alltid är en kompromiss. Det finns ingen idealisk arkitektur och det är inte möjligt att ta hänsyn till alla scenarier för applikationsutvecklingen i förväg. Arkitekturkvaliteten beror på flera parametrar, ofta uteslutande. Du kan lösa alla designproblem genom att lägga till en extra abstraktionsnivå. Den hänvisar dock inte till problemet med en enorm mängd abstraktionsnivåer. Jag rekommenderar inte att man tänker att interaktion mellan objekt enbart bygger på abstraktioner. Poängen är att du använder koden som tillåter interaktion mellan implementeringar och är mindre flexibel, vilket gör att den inte har möjlighet att testas med enhetstester.


  1. När indexerar Oracle kolumnvärden null?

  2. Oracle Connection Pool Class

  3. Navigat för MySQL

  4. Hur man beräknar omvandlingsfrekvens i MySQL?