Реших да напиша тази статия, за да покажа, че модулните тестове са не само инструмент за справяне с регресията в кода, но също така са страхотна инвестиция във висококачествена архитектура. Освен това една тема в английската .NET общност ме мотивира да направя това. Автор на статията е Джони. Той описа своя първи и последен ден в компанията, занимаваща се с разработка на софтуер за бизнес във финансовия сектор. Джони кандидатстваше за позицията – разработчик на модулни тестове. Той беше разстроен от лошото качество на кода, което трябваше да тества. Той сравни кода с боклук, пълен с обекти, които се клонират един друг на всякакви неподходящи места. В допълнение, той не можа да намери абстрактни типове данни в хранилище:кодът съдържаше само обвързване на реализации, които се изискват взаимно.
Джони, осъзнавайки цялата безполезност на модулното тестване в тази компания, очерта тази ситуация на мениджъра, отказа от по-нататъшно сътрудничество и даде ценен съвет. Той препоръча на екип за разработка да отиде на курсове, за да научи инстанциране на обекти и използване на абстрактни типове данни. Не знам дали мениджърът последва съвета му (мисля, че не го направи). Въпреки това, ако се интересувате какво е имал предвид Джони и как използването на модулно тестване може да повлияе на качеството на вашата архитектура, можете да прочетете тази статия.
Изолирането на зависимости е основа за тестване на модули
Модулният или модулен тест е тест, който проверява функционалността на модула, изолирана от неговите зависимости. Изолирането на зависимостта е замяна на обекти от реалния свят, с които взаимодейства тестваният модул, с мъничета, които симулират правилното поведение на техните прототипи. Това заместване позволява да се фокусира върху тестването на конкретен модул, като се игнорира възможно неправилно поведение на неговата среда. Необходимостта от замяна на зависимостите в теста предизвиква интересно свойство. Разработчик, който осъзнава, че техният код ще бъде използван в модулни тестове, трябва да разработи с помощта на абстракции и да извърши рефакторинг при първите признаци на висока свързаност.
Ще го разгледам на конкретния пример.
Нека се опитаме да си представим как може да изглежда модул за лични съобщения в система, разработена от компанията, от която Джони избяга. И как би изглеждал същият модул, ако разработчиците приложат тестване на модули.
Модулът трябва да може да съхранява съобщението в базата данни и ако лицето, до което е адресирано съобщението, е в системата — показва съобщението на екрана с известие за тост.
//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); } } }
Нека проверим какви зависимости има нашият модул.
Функцията SendMessage извиква статични методи на обектите Notificationsservice и Usersservice и създава обекта Messagesrepository, който отговаря за работата с базата данни.
Няма проблем с факта, че модулът взаимодейства с други обекти. Проблемът е как се изгражда това взаимодействие и то не е изградено успешно. Директният достъп до методи на трети страни направи нашия модул тясно свързан със специфични реализации.
Това взаимодействие има много недостатъци, но важното е, че модулът Messagingservice е загубил възможността да бъде тестван изолирано от реализациите на Notificationsservice, Usersservice и Messagesrepository. Всъщност не можем да заменим тези обекти с мъничета.
Сега нека разгледаме как би изглеждал същият модул, ако разработчикът се погрижи за него.
//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); } } }
Както можете да видите, тази версия е много по-добра. Взаимодействието между обектите вече се изгражда не директно, а чрез интерфейси.
Вече нямаме нужда от достъп до статични класове и инстанциране на обекти в методи с бизнес логика. Основният момент е, че можем да заменим всички зависимости, като предадем мъничета за тестване в конструктор. По този начин, като подобряваме тестуемостта на кода, бихме могли да подобрим както тестуемостта на нашия код, така и архитектурата на нашето приложение. Отказахме директно използване на реализации и предадохме инстанциране на горния слой. Точно това искаше Джони.
След това създайте тест за модула за изпращане на съобщения.
Спецификация на тестовете
Определете какво трябва да провери нашият тест:
- Едно извикване на метода SaveMessage
- Едно извикване на метода SendNotificationToUser(), ако заглушката на метода IsUserOnline() над обекта IUsersService връща true
- Няма метод SendNotificationToUser(), ако заглушката на метода IsUserOnline() над обекта IUsersService връща false
Спазването на тези условия може да гарантира, че изпълнението на съобщението SendMessage е правилно и не съдържа грешки.
Тестове
Тестът се изпълнява с помощта на изолирана рамка 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); }
За да обобщим, търсенето на идеална архитектура е безполезна задача.
Единичните тестове са чудесни за използване, когато трябва да проверите архитектурата при загуба на свързване между модулите. Все пак имайте предвид, че проектирането на сложни инженерни системи винаги е компромис. Няма идеална архитектура и не е възможно предварително да се вземат предвид всички сценарии на разработка на приложението. Качеството на архитектурата зависи от множество параметри, често взаимно изключващи се. Можете да решите всеки проблем с дизайна, като добавите допълнително ниво на абстракция. Това обаче не се отнася до проблема с огромно количество нива на абстракция. Не препоръчвам да мислите, че взаимодействието между обектите се основава само на абстракции. Въпросът е, че използвате кода, който позволява взаимодействие между имплементациите и е по-малко гъвкав, което означава, че няма възможност да бъде тестван чрез модулни тестове.