Database
 sql >> Datenbank >  >> RDS >> Database

Warum die Verwendung von Unit-Tests eine großartige Investition in eine hochwertige Architektur ist

Ich habe mich entschieden, diesen Artikel zu schreiben, um zu zeigen, dass Unit-Tests nicht nur ein Werkzeug sind, um mit Regression im Code fertig zu werden, sondern auch eine großartige Investition in eine qualitativ hochwertige Architektur sind. Außerdem hat mich ein Thema in der englischen .NET-Community dazu motiviert. Der Autor des Artikels war Johnnie. Er beschrieb seinen ersten und letzten Tag im Unternehmen, das sich mit der Softwareentwicklung für Unternehmen im Finanzsektor befasste. Johnnie bewarb sich auf die Stelle – eines Entwicklers von Unit-Tests. Er ärgerte sich über die schlechte Codequalität, die er testen musste. Er verglich den Code mit einem Schrottplatz, vollgestopft mit Objekten, die sich an ungeeigneten Stellen gegenseitig klonen. Außerdem konnte er keine abstrakten Datentypen in einem Repository finden:Der Code enthielt nur Bindungen von Implementierungen, die sich gegenseitig anfordern.

Johnnie, der die ganze Nutzlosigkeit von Modultests in diesem Unternehmen erkannte, skizzierte diese Situation dem Manager, lehnte eine weitere Zusammenarbeit ab und gab einen wertvollen Rat. Er empfahl einem Entwicklungsteam, an Kursen teilzunehmen, um das Instanziieren von Objekten und die Verwendung abstrakter Datentypen zu erlernen. Ich weiß nicht, ob der Manager seinem Rat gefolgt ist (ich glaube, er hat es nicht getan). Wenn Sie jedoch interessiert sind, was Johnnie gemeint hat und wie die Verwendung von Modultests die Qualität Ihrer Architektur beeinflussen kann, können Sie gerne diesen Artikel lesen.

Abhängigkeitsisolation ist eine Basis für Modultests

Modul- oder Unit-Test ist ein Test, der die Funktionalität des Moduls isoliert von seinen Abhängigkeiten verifiziert. Abhängigkeitsisolation ist ein Ersatz von realen Objekten, mit denen das getestete Modul interagiert, durch Stubs, die das korrekte Verhalten ihrer Prototypen simulieren. Diese Substitution ermöglicht es, sich auf das Testen eines bestimmten Moduls zu konzentrieren und ein mögliches falsches Verhalten seiner Umgebung zu ignorieren. Eine Notwendigkeit, Abhängigkeiten im Test zu ersetzen, verursacht eine interessante Eigenschaft. Ein Entwickler, der erkennt, dass sein Code in Modultests verwendet wird, muss mithilfe von Abstraktionen entwickeln und bei den ersten Anzeichen einer hohen Konnektivität ein Refactoring durchführen.

Ich werde es an dem speziellen Beispiel betrachten.

Versuchen wir uns vorzustellen, wie ein persönliches Nachrichtenmodul auf einem System aussehen könnte, das von der Firma entwickelt wurde, aus der Johnnie geflohen ist. Und wie dasselbe Modul aussehen würde, wenn Entwickler Komponententests anwenden würden.

Das Modul sollte in der Lage sein, die Nachricht in der Datenbank zu speichern, und wenn die Person, an die die Nachricht adressiert war, im System vorhanden ist, die Nachricht auf dem Bildschirm mit einer Toastbenachrichtigung anzeigen.

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

Lassen Sie uns überprüfen, welche Abhängigkeiten unser Modul hat.

Die SendMessage-Funktion ruft statische Methoden der Notificationsservice- und Usersservice-Objekte auf und erstellt das Messagesrepository-Objekt, das für die Arbeit mit der Datenbank verantwortlich ist.

Dass das Modul mit anderen Objekten interagiert, ist kein Problem. Das Problem ist, wie diese Interaktion aufgebaut ist, und sie ist nicht erfolgreich aufgebaut. Der direkte Zugriff auf Methoden von Drittanbietern hat unser Modul eng mit bestimmten Implementierungen verknüpft.

Diese Interaktion hat viele Nachteile, aber das Wichtigste ist, dass das Messagingservice-Modul nicht mehr isoliert von den Implementierungen von Notificationsservice, Usersservice und Messagesrepository getestet werden kann. Eigentlich können wir diese Objekte nicht durch Stubs ersetzen.

Schauen wir uns nun an, wie dasselbe Modul aussehen würde, wenn sich ein Entwickler darum kümmern würde.

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

Wie Sie sehen können, ist diese Version viel besser. Die Interaktion zwischen Objekten wird jetzt nicht direkt, sondern über Schnittstellen aufgebaut.

Wir müssen nicht mehr auf statische Klassen zugreifen und Objekte in Methoden mit Geschäftslogik instanziieren. Der Hauptpunkt ist, dass wir alle Abhängigkeiten ersetzen können, indem wir Stubs zum Testen an einen Konstruktor übergeben. Somit könnten wir, während wir die Testbarkeit des Codes verbessern, auch sowohl die Testbarkeit unseres Codes als auch die Architektur unserer Anwendung verbessern. Wir lehnten die direkte Verwendung von Implementierungen ab und übergaben die Instanziierung an die darüber liegende Schicht. Genau das wollte Johnnie.

Erstellen Sie als nächstes einen Test für das Modul zum Senden von Nachrichten.

Spezifikation zu Tests

Definieren Sie, was unser Test überprüfen soll:

  • Ein einzelner Aufruf der SaveMessage-Methode
  • Ein einzelner Aufruf der SendNotificationToUser()-Methode, wenn der IsUserOnline()-Methoden-Stub über das IUsersService-Objekt true zurückgibt
  • Es gibt keine SendNotificationToUser()-Methode, wenn der IsUserOnline()-Methoden-Stub über das IUsersService-Objekt falsch zurückgibt

Die Einhaltung dieser Bedingungen kann garantieren, dass die Implementierung der SendMessage-Nachricht korrekt ist und keine Fehler enthält.

Tests

Der Test wird mithilfe des isolierten Moq-Frameworks

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

Zusammenfassend lässt sich sagen, dass die Suche nach einer idealen Architektur eine nutzlose Aufgabe ist.

Komponententests eignen sich hervorragend, wenn Sie die Architektur auf lose Kopplung zwischen Modulen überprüfen müssen. Denken Sie jedoch daran, dass das Entwerfen komplexer technischer Systeme immer ein Kompromiss ist. Es gibt keine ideale Architektur und es ist nicht möglich, alle Szenarien der Anwendungsentwicklung im Voraus zu berücksichtigen. Die Qualität der Architektur hängt von mehreren Parametern ab, die sich oft gegenseitig ausschließen. Sie können jedes Designproblem lösen, indem Sie eine zusätzliche Abstraktionsebene hinzufügen. Es bezieht sich jedoch nicht auf das Problem einer riesigen Menge an Abstraktionsebenen. Ich empfehle nicht zu denken, dass die Interaktion zwischen Objekten nur auf Abstraktionen basiert. Der Punkt ist, dass Sie den Code verwenden, der eine Interaktion zwischen Implementierungen zulässt und weniger flexibel ist, was bedeutet, dass er nicht durch Unit-Tests getestet werden kann.