Database
 sql >> Baza danych >  >> RDS >> Database

Dlaczego korzystanie z testów jednostkowych to świetna inwestycja w architekturę wysokiej jakości

Postanowiłem napisać ten artykuł, aby pokazać, że testy jednostkowe to nie tylko narzędzie do zmagania się z regresją w kodzie, ale także świetna inwestycja w wysokiej jakości architekturę. Dodatkowo zmotywował mnie do tego temat w anglojęzycznej społeczności .NET. Autorem artykułu był Johnnie. Opisał swój pierwszy i ostatni dzień w firmie zajmującej się tworzeniem oprogramowania dla biznesu w sektorze finansowym. Johnnie ubiegał się o stanowisko – programisty testów jednostkowych. Był zdenerwowany słabą jakością kodu, który musiał przetestować. Porównał kod ze złomowiskiem wypełnionym przedmiotami, które klonują się nawzajem w nieodpowiednich miejscach. Ponadto nie mógł znaleźć abstrakcyjnych typów danych w repozytorium:kod zawierał tylko powiązania implementacji, które krzyżują się ze sobą.

Johnnie zdając sobie sprawę z bezużyteczności testowania modułów w tej firmie nakreślił tę sytuację kierownikowi, odmówił dalszej współpracy i udzielił cennej rady. Zalecił, aby zespół programistów przeszedł kursy na temat tworzenia instancji obiektów i używania abstrakcyjnych typów danych. Nie wiem, czy kierownik zastosował się do jego rady (chyba nie). Jeśli jednak interesuje Cię, co miał na myśli Johnnie i jak testowanie modułów może wpłynąć na jakość Twojej architektury, zapraszamy do przeczytania tego artykułu.

Izolacja zależności jest podstawą testowania modułów

Test modułu lub jednostki to test, który weryfikuje funkcjonalność modułu oddzieloną od jego zależności. Izolacja zależności polega na zastąpieniu rzeczywistych obiektów, z którymi testowany moduł współdziała, za pomocą skrótów, które symulują prawidłowe zachowanie ich prototypów. Ta podmiana pozwala skupić się na testowaniu konkretnego modułu, ignorując ewentualne nieprawidłowe zachowanie jego środowiska. Konieczność zastąpienia zależności w teście powoduje ciekawą właściwość. Programista, który zdaje sobie sprawę, że jego kod będzie używany w testach modułów, musi rozwijać się przy użyciu abstrakcji i przeprowadzać refaktoryzację przy pierwszych oznakach wysokiej łączności.

Rozważę to na konkretnym przykładzie.

Spróbujmy sobie wyobrazić, jak mógłby wyglądać moduł wiadomości osobistych w systemie opracowanym przez firmę, z której uciekł Johnnie. I jak wyglądałby ten sam moduł, gdyby programiści stosowali testy jednostkowe.

Moduł powinien być w stanie przechowywać wiadomość w bazie danych, a jeśli osoba, do której wiadomość była adresowana, jest w systemie — wyświetlić wiadomość na ekranie z wyskakującym powiadomieniem.

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

Sprawdźmy, jakie zależności ma nasz moduł.

Funkcja SendMessage wywołuje statyczne metody obiektów Notificationsservice i Usersservice i tworzy obiekt Messagesrepository odpowiedzialny za pracę z bazą danych.

Nie ma problemu z tym, że moduł współdziała z innymi obiektami. Problem polega na tym, jak ta interakcja jest budowana i nie jest budowana pomyślnie. Bezpośredni dostęp do metod innych firm sprawił, że nasz moduł jest ściśle powiązany z określonymi implementacjami.

Ta interakcja ma wiele wad, ale ważne jest to, że moduł Messagingservice stracił możliwość testowania w oderwaniu od implementacji Notificationsservice, Usersservice i Messagesrepository. Właściwie nie możemy zastąpić tych obiektów kodami pośredniczącymi.

Teraz spójrzmy, jak wyglądałby ten sam moduł, gdyby zajął się nim programista.

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

Jak widać, ta wersja jest znacznie lepsza. Interakcja między obiektami jest teraz budowana nie bezpośrednio, ale poprzez interfejsy.

Nie musimy już uzyskiwać dostępu do klas statycznych i tworzyć instancji obiektów w metodach z logiką biznesową. Najważniejsze jest to, że możemy zastąpić wszystkie zależności, przekazując kody pośredniczące do testowania do konstruktora. W ten sposób, zwiększając testowalność kodu, możemy również poprawić zarówno testowalność naszego kodu, jak i architekturę naszej aplikacji. Odmówiliśmy bezpośredniego używania implementacji i przekazaliśmy tworzenie instancji do warstwy powyżej. Właśnie tego chciał Johnnie.

Następnie utwórz test dla modułu wysyłania wiadomości.

Specyfikacja testów

Określ, co nasz test powinien sprawdzić:

  • Pojedyncze wywołanie metody SaveMessage
  • Pojedyncze wywołanie metody SendNotificationToUser(), jeśli skrót metody IsUserOnline() nad obiektem IUsersService zwróci true
  • Nie ma metody SendNotificationToUser(), jeśli skrót metody IsUserOnline() nad obiektem IUsersService zwraca wartość false

Przestrzeganie tych warunków może zagwarantować, że implementacja wiadomości SendMessage jest poprawna i nie zawiera żadnych błędów.

Testy

Test jest zaimplementowany przy użyciu izolowanego frameworka Moq

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

Podsumowując, szukanie idealnej architektury jest bezużytecznym zadaniem.

Testy jednostkowe są świetne do użycia, gdy trzeba sprawdzić architekturę pod kątem luźnego sprzężenia między modułami. Należy jednak pamiętać, że projektowanie złożonych systemów inżynierskich jest zawsze kompromisem. Nie ma idealnej architektury i nie jest możliwe wcześniejsze uwzględnienie wszystkich scenariuszy rozwoju aplikacji. Jakość architektury zależy od wielu parametrów, często wzajemnie się wykluczających. Możesz rozwiązać każdy problem projektowy, dodając dodatkowy poziom abstrakcji. Nie odnosi się to jednak do problemu ogromnej ilości poziomów abstrakcji. Nie radzę myśleć, że interakcja między przedmiotami opiera się wyłącznie na abstrakcji. Chodzi o to, że używasz kodu, który umożliwia interakcję między implementacjami i jest mniej elastyczny, co oznacza, że ​​nie ma możliwości przetestowania w testach jednostkowych.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Która baza danych szeregów czasowych jest lepsza:TimescaleDB a InfluxDB

  2. Instrukcja SQL SELECT INTO

  3. Knee-Jerk PerfMon Liczniki:Oczekiwana długość życia strony

  4. Luka w zabezpieczeniach Joomla SQL iniekcji

  5. Przywróć swoją bazę danych WordPress za pomocą WP-CLI