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.