sql >> Databáze >  >> RDS >> Database

Proč je používání jednotkových testů skvělou investicí do vysoce kvalitní architektury

Rozhodl jsem se napsat tento článek, abych ukázal, že unit testy nejsou pouze nástrojem pro boj s regresí v kódu, ale jsou také skvělou investicí do vysoce kvalitní architektury. Navíc mě k tomu motivovalo téma v anglické .NET komunitě. Autorem článku byl Johnnie. Popsal svůj první a poslední den ve společnosti zabývající se vývojem softwaru pro podnikání ve finančním sektoru. Johnnie se ucházel o pozici vývojáře unit testů. Byl naštvaný na špatnou kvalitu kódu, který musel otestovat. Porovnal kód s vrakovištěm nacpaným předměty, které se navzájem klonují na jakýchkoli nevhodných místech. Kromě toho nemohl v úložišti najít abstraktní datové typy:kód obsahoval pouze vazby implementací, které si navzájem křížově žádají.

Johnnie, který si uvědomil veškerou zbytečnost testování modulů v této společnosti, tuto situaci nastínil manažerovi, odmítl další spolupráci a poskytl cennou radu. Doporučil, aby vývojový tým absolvoval kurzy, kde se naučí vytvářet instance objektů a používat abstraktní datové typy. Nevím, zda se manažer řídil jeho radou (myslím, že ne). Pokud vás však zajímá, co tím Johnnie myslel a jak může testování modulů ovlivnit kvalitu vaší architektury, můžete si přečíst tento článek.

Izolace závislostí je základem testování modulů

Modul nebo unit test je test, který ověřuje funkčnost modulu izolovanou od jeho závislostí. Izolace závislostí je substituce objektů reálného světa, se kterými testovaný modul interaguje, útržky, které simulují správné chování jejich prototypů. Tato náhrada umožňuje zaměřit se na testování konkrétního modulu a ignorovat možné nesprávné chování jeho prostředí. Nutnost nahradit v testu závislosti způsobuje zajímavou vlastnost. Vývojář, který si uvědomuje, že jeho kód bude použit v testech modulů, musí vyvíjet pomocí abstrakcí a provést refaktoring při prvních známkách vysoké konektivity.

Budu to zvažovat na konkrétním příkladu.

Zkusme si představit, jak by mohl vypadat modul osobních zpráv na systému vyvinutém společností, ze které Johnnie utekl. A jak by stejný modul vypadal, kdyby vývojáři použili testování jednotek.

Modul by měl být schopen uložit zprávu do databáze a pokud je osoba, které byla zpráva určena, v systému — zobrazit zprávu na obrazovce s upozorněním na toast.

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

Pojďme zkontrolovat, jaké závislosti má náš modul.

Funkce SendMessage vyvolá statické metody objektů Notificationsservice a Usersservice a vytvoří objekt Messagesrepository, který je zodpovědný za práci s databází.

Není problém s tím, že modul interaguje s jinými objekty. Problém je v tom, jak je tato interakce vytvořena, a není úspěšně vybudována. Díky přímému přístupu k metodám třetích stran je náš modul pevně propojen s konkrétními implementacemi.

Tato interakce má spoustu stinných stránek, ale důležité je, že modul Messagingservice ztratil možnost testování izolovaně od implementací Notificationsservice, Usersservice a Messagesrepository. Ve skutečnosti tyto objekty nemůžeme nahradit útržky.

Nyní se podívejme, jak by stejný modul vypadal, kdyby se o něj staral vývojář.

//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 vidíte, tato verze je mnohem lepší. Interakce mezi objekty se nyní nevytváří přímo, ale prostřednictvím rozhraní.

Již nepotřebujeme přistupovat ke statickým třídám a vytvářet instance objektů v metodách s obchodní logikou. Hlavním bodem je, že můžeme nahradit všechny závislosti předáním útržků k testování do konstruktoru. Při zlepšení testovatelnosti kódu bychom tedy mohli zlepšit testovatelnost našeho kódu i architekturu naší aplikace. Odmítli jsme přímé používání implementací a předali jsme konkretizaci výše uvedené vrstvě. To je přesně to, co Johnnie chtěl.

Dále vytvořte test pro modul odesílání zpráv.

Specifikace testů

Definujte, co má náš test zkontrolovat:

  • Jedno volání metody SaveMessage
  • Jedno volání metody SendNotificationToUser(), pokud stub metody IsUserOnline() nad objektem IUsersService vrátí hodnotu true
  • Neexistuje žádná metoda SendNotificationToUser(), pokud útržek metody IsUserOnline() nad objektem IUsersService vrací hodnotu false

Dodržování těchto podmínek může zaručit, že implementace zprávy SendMessage je správná a neobsahuje žádné chyby.

Testy

Test je implementován pomocí izolovaného rámce 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);
}

Když to shrnu, hledat ideální architekturu je zbytečný úkol.

Unit testy jsou skvělé pro použití, když potřebujete zkontrolovat architekturu na ztrátu vazby mezi moduly. Přesto mějte na paměti, že navrhování složitých inženýrských systémů je vždy kompromisem. Ideální architektura neexistuje a není možné předem zohlednit všechny scénáře vývoje aplikace. Kvalita architektury závisí na mnoha parametrech, které se často vzájemně vylučují. Jakýkoli problém s návrhem můžete vyřešit přidáním další úrovně abstrakce. Netýká se však problému velkého množství úrovní abstrakce. Nedoporučuji si myslet, že interakce mezi objekty je založena pouze na abstrakcích. Jde o to, že používáte kód, který umožňuje interakci mezi implementacemi a je méně flexibilní, což znamená, že jej nelze testovat jednotkovými testy.


  1. skalární poddotaz v příkazu if Podmínka v PL/SQL

  2. Přidat vypočítaný sloupec do existující tabulky na serveru SQL Server

  3. Prům. nekonzistence plováku

  4. Pořadí jako výchozí hodnota pro sloupec