sql >> Database >  >> RDS >> Database

Waarom het gebruik van eenheidstests een geweldige investering is in hoogwaardige architectuur

Ik heb besloten dit artikel te schrijven om te laten zien dat unit tests niet alleen een hulpmiddel zijn om regressie in de code aan te pakken, maar ook een geweldige investering zijn in een hoogwaardige architectuur. Daarnaast motiveerde een onderwerp in de Engelse .NET community mij om dit te doen. De auteur van het artikel was Johnnie. Hij beschreef zijn eerste en laatste dag in het bedrijf dat zich bezighoudt met softwareontwikkeling voor het bedrijfsleven in de financiële sector. Johnnie solliciteerde naar de functie van ontwikkelaar van unit-tests. Hij was boos over de slechte kwaliteit van de code, die hij moest testen. Hij vergeleek de code met een autokerkhof vol met objecten die elkaar op ongeschikte plaatsen klonen. Bovendien kon hij geen abstracte datatypes vinden in een repository:de code bevatte alleen binding van implementaties die elkaar kruisen.

Johnnie realiseerde zich hoe nutteloos het testen van modules in dit bedrijf was, schetste deze situatie aan de manager, weigerde verdere medewerking en gaf een waardevol advies. Hij raadde een ontwikkelteam aan cursussen te volgen om te leren hoe objecten te instantiëren en abstracte gegevenstypen te gebruiken. Ik weet niet of de manager zijn advies heeft opgevolgd (ik denk van niet). Als je echter geïnteresseerd bent in wat Johnnie bedoelde en hoe het gebruik van moduletests de kwaliteit van je architectuur kan beïnvloeden, lees dan dit artikel.

Afhankelijkheidsisolatie is een basis voor het testen van modules

Module- of eenheidstest is een test die de modulefunctionaliteit verifieert die is geïsoleerd van zijn afhankelijkheden. Afhankelijkheidsisolatie is een vervanging van objecten uit de echte wereld, waarmee de geteste module samenwerkt, met stubs die het juiste gedrag van hun prototypen simuleren. Met deze vervanging kunt u zich concentreren op het testen van een bepaalde module, waarbij mogelijk onjuist gedrag van de omgeving wordt genegeerd. Een noodzaak om afhankelijkheden in de test te vervangen, veroorzaakt een interessante eigenschap. Een ontwikkelaar die zich realiseert dat zijn code zal worden gebruikt in moduletests, moet ontwikkelen met behulp van abstracties en refactoring uitvoeren bij de eerste tekenen van hoge connectiviteit.

Ik ga het over het specifieke voorbeeld bekijken.

Laten we ons eens voorstellen hoe een persoonlijke berichtenmodule eruit zou kunnen zien op een systeem dat is ontwikkeld door het bedrijf waaruit Johnnie is ontsnapt. En hoe dezelfde module eruit zou zien als ontwikkelaars unit testing zouden toepassen.

De module moet het bericht in de database kunnen opslaan en als de persoon aan wie het bericht is geadresseerd zich in het systeem bevindt — het bericht op het scherm weergeven met een toastmelding.

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

Laten we eens kijken welke afhankelijkheden onze module heeft.

De functie SendMessage roept statische methoden aan van de objecten Notificationsservice en Usersservice en creëert het object Messagesrepository dat verantwoordelijk is voor het werken met de database.

Er is geen probleem met het feit dat de module interageert met andere objecten. Het probleem is hoe deze interactie is opgebouwd, en het is niet succesvol gebouwd. Door directe toegang tot methoden van derden is onze module nauw verbonden met specifieke implementaties.

Deze interactie heeft veel nadelen, maar het belangrijkste is dat de Messagingservice-module de mogelijkheid heeft verloren om geïsoleerd te worden getest van de implementaties van de Notificationsservice, Usersservice en Messagesrepository. Eigenlijk kunnen we deze objecten niet vervangen door stubs.

Laten we nu eens kijken hoe dezelfde module eruit zou zien als een ontwikkelaar ervoor zou zorgen.

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

Zoals je kunt zien, is deze versie veel beter. De interactie tussen objecten wordt nu niet rechtstreeks gebouwd, maar via interfaces.

We hoeven geen toegang meer te krijgen tot statische klassen en objecten te instantiëren in methoden met bedrijfslogica. Het belangrijkste punt is dat we alle afhankelijkheden kunnen vervangen door stubs voor testen door te geven aan een constructor. Dus, terwijl we de testbaarheid van de code verbeteren, kunnen we ook zowel de testbaarheid van onze code als de architectuur van onze applicatie verbeteren. We weigerden direct gebruik te maken van implementaties en gaven instantiëring door aan de laag erboven. Dit is precies wat Johnnie wilde.

Maak vervolgens een test voor de module voor het verzenden van berichten.

Specificatie op tests

Definieer wat onze test moet controleren:

  • Een enkele aanroep van de SaveMessage-methode
  • Een enkele aanroep van de methode SendNotificationToUser() als de stub van de methode IsUserOnline() over het IUsersService-object true retourneert
  • Er is geen SendNotificationToUser()-methode als de stub van de IsUserOnline()-methode over het IUsersService-object false retourneert

Het volgen van deze voorwaarden kan garanderen dat de implementatie van het SendMessage-bericht correct is en geen fouten bevat.

Testen

De test wordt geïmplementeerd met behulp van het geïsoleerde Moq-framework

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

Kortom, het zoeken naar een ideale architectuur is een nutteloze taak.

Unit-tests zijn geweldig om te gebruiken wanneer u de architectuur moet controleren op losse koppeling tussen modules. Houd er echter rekening mee dat het ontwerpen van complexe technische systemen altijd een compromis is. Er is geen ideale architectuur en het is niet mogelijk om vooraf rekening te houden met alle scenario's van de applicatieontwikkeling. De kwaliteit van de architectuur hangt af van meerdere parameters, die elkaar vaak uitsluiten. U kunt elk ontwerpprobleem oplossen door een extra abstractieniveau toe te voegen. Het verwijst echter niet naar het probleem van een enorme hoeveelheid abstractieniveaus. Ik raad niet aan te denken dat interactie tussen objecten alleen gebaseerd is op abstracties. Het punt is dat je de code gebruikt die interactie tussen implementaties mogelijk maakt en minder flexibel is, wat betekent dat het niet de mogelijkheid heeft om getest te worden door unit tests.


  1. Hoe u een lijst met alle controlebeperkingen in de SQL Server-database kunt krijgen - SQL Server / TSQL-zelfstudie, deel 85

  2. Virtualisering inschakelen in BIOS op laptop of desktop voor Virtualbox VM

  3. CONTROLEER BEPERKING op meerdere kolommen

  4. Is het mogelijk om naar één kolom te verwijzen als meerdere externe sleutels?