Jeg har besluttet at skrive denne artikel for at vise, at enhedstests ikke kun er et værktøj til at kæmpe med regression i koden, men også er en stor investering i en arkitektur af høj kvalitet. Derudover motiverede et emne i det engelske .NET-fællesskab mig til at gøre dette. Forfatteren til artiklen var Johnnie. Han beskrev sin første og sidste dag i virksomheden involveret i softwareudvikling til erhvervslivet i den finansielle sektor. Johnnie søgte stillingen – som udvikler af enhedstests. Han var ked af den dårlige kodekvalitet, som han skulle teste. Han sammenlignede koden med en skraldeplads fyldt med genstande, der kloner hinanden på uegnede steder. Derudover kunne han ikke finde abstrakte datatyper i et lager:koden indeholdt kun binding af implementeringer, der krydsanmoder hinanden.
Johnnie, der indså al nytteløsheden af modultestning i denne virksomhed, skitserede denne situation for lederen, nægtede yderligere samarbejde og gav et værdifuldt råd. Han anbefalede, at et udviklingsteam gik på kurser for at lære at instansiere objekter og bruge abstrakte datatyper. Jeg ved ikke, om lederen fulgte hans råd (det tror jeg ikke, han gjorde). Men hvis du er interesseret i, hvad Johnnie mente, og hvordan brug af modultestning kan påvirke kvaliteten af din arkitektur, er du velkommen til at læse denne artikel.
Afhængighedsisolering er en base for modultestning
Modul- eller enhedstest er en test, der verificerer modulets funktionalitet isoleret fra dets afhængigheder. Afhængighedsisolation er en substitution af objekter fra den virkelige verden, som modulet, der testes, interagerer med, med stubbe, der simulerer den korrekte adfærd af deres prototyper. Denne substitution gør det muligt at fokusere på at teste et bestemt modul og ignorere en mulig forkert adfærd i dets omgivelser. En nødvendighed for at erstatte afhængigheder i testen forårsager en interessant egenskab. En udvikler, der indser, at deres kode vil blive brugt i modultest, skal udvikle ved hjælp af abstraktioner og udføre refactoring ved de første tegn på høj forbindelse.
Jeg vil overveje det i det konkrete eksempel.
Lad os prøve at forestille os, hvordan et personligt beskedmodul kan se ud på et system udviklet af det firma, som Johnnie flygtede fra. Og hvordan det samme modul ville se ud, hvis udviklere skulle anvende enhedstest.
Modulet skal være i stand til at gemme beskeden i databasen, og hvis den person, som beskeden var adresseret til, er i systemet — vis beskeden på skærmen med en toast-notifikation.
//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); } } }
Lad os tjekke, hvilke afhængigheder vores modul har.
SendMessage-funktionen påkalder statiske metoder for Notificationsservice- og Usersservice-objekterne og opretter Messagesrepository-objektet, der er ansvarligt for at arbejde med databasen.
Der er ingen problemer med, at modulet interagerer med andre objekter. Problemet er, hvordan denne interaktion er bygget, og den er ikke bygget med succes. Direkte adgang til tredjepartsmetoder har gjort vores modul tæt knyttet til specifikke implementeringer.
Denne interaktion har mange ulemper, men det vigtige er, at Messagingservice-modulet har mistet muligheden for at blive testet isoleret fra implementeringerne af Notificationsservice, Usersservice og Messagesrepository. Faktisk kan vi ikke erstatte disse objekter med stubbe.
Lad os nu se på, hvordan det samme modul ville se ud, hvis en udvikler skulle tage sig af det.
//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); } } }
Som du kan se, er denne version meget bedre. Interaktionen mellem objekter bygges nu ikke direkte, men gennem grænseflader.
Vi behøver ikke længere at få adgang til statiske klasser og instansiere objekter i metoder med forretningslogik. Hovedpointen er, at vi kan erstatte alle afhængigheder ved at overføre stubs til test i en konstruktør. Mens vi forbedrer kodetestbarheden, kunne vi således også forbedre både testbarheden af vores kode og arkitekturen af vores applikation. Vi nægtede at bruge implementeringer direkte og sendte instansiering til laget ovenfor. Det er præcis, hvad Johnnie ønskede.
Opret derefter en test for modulet til at sende beskeder.
Specifikation om test
Definer, hvad vores test skal kontrollere:
- Et enkelt kald af SaveMessage-metoden
- Et enkelt kald af SendNotificationToUser()-metoden, hvis IsUserOnline()-metodestubben over IUsersService-objektet returnerer true
- Der er ingen SendNotificationToUser()-metode, hvis IsUserOnline()-metodestubben over IUsersService-objektet returnerer false
At følge disse betingelser kan garantere, at implementeringen af SendMessage-meddelelsen er korrekt og ikke indeholder nogen fejl.
Tests
Testen er implementeret ved hjælp af den isolerede Moq-ramme
[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); }
For at opsummere er det en ubrugelig opgave at lede efter en ideel arkitektur.
Enhedstests er gode at bruge, når du skal tjekke arkitekturen ved tab af kobling mellem moduler. Alligevel skal du huske på, at design af komplekse tekniske systemer altid er et kompromis. Der er ingen ideel arkitektur, og det er ikke muligt at tage højde for alle scenarierne i applikationsudviklingen på forhånd. Arkitekturens kvalitet afhænger af flere parametre, som ofte udelukker hinanden. Du kan løse ethvert designproblem ved at tilføje et ekstra abstraktionsniveau. Det refererer dog ikke til problemet med en enorm mængde abstraktionsniveauer. Jeg anbefaler ikke at tro, at interaktion mellem objekter kun er baseret på abstraktioner. Pointen er, at du bruger koden, der tillader interaktion mellem implementeringer og er mindre fleksibel, hvilket betyder, at den ikke har mulighed for at blive testet ved enhedstests.