Jag har bestämt mig för att skriva den här artikeln för att visa att enhetstester inte bara är ett verktyg för att brottas med regression i koden utan också är en stor investering i en högkvalitativ arkitektur. Dessutom motiverade ett ämne i det engelska .NET-communityt mig att göra detta. Författaren till artikeln var Johnnie. Han beskrev sin första och sista dag i företaget som är involverat i mjukvaruutveckling för företag inom finanssektorn. Johnnie sökte tjänsten – som utvecklare av enhetstester. Han var upprörd över den dåliga kodkvaliteten, som han var tvungen att testa. Han jämförde koden med en skrotupplag fylld med föremål som klonar varandra på alla olämpliga ställen. Dessutom kunde han inte hitta abstrakta datatyper i ett arkiv:koden innehöll bara bindning av implementeringar som korsar begär varandra.
Johnnie insåg all värdelöshet med modultestning i det här företaget beskrev denna situation för chefen, vägrade från ytterligare samarbete och gav ett värdefullt råd. Han rekommenderade att ett utvecklingsteam skulle gå på kurser för att lära sig instansiera objekt och använda abstrakta datatyper. Jag vet inte om chefen följde hans råd (jag tror att han inte gjorde det). Men om du är intresserad av vad Johnnie menade och hur användning av modultestning kan påverka kvaliteten på din arkitektur, är du välkommen att läsa den här artikeln.
Beroendeisolering är en bas för modultestning
Modul- eller enhetstest är ett test som verifierar modulens funktionalitet isolerad från dess beroenden. Beroendeisolering är en ersättning av verkliga objekt, som modulen som testas interagerar med, med stubbar som simulerar det korrekta beteendet hos deras prototyper. Denna ersättning gör det möjligt att fokusera på att testa en viss modul, och ignorera ett eventuellt felaktigt beteende i dess miljö. En nödvändighet att ersätta beroenden i testet orsakar en intressant egenskap. En utvecklare som inser att deras kod kommer att användas i modultester måste utveckla med hjälp av abstraktioner och utföra refactoring vid de första tecknen på hög anslutning.
Jag kommer att överväga det på det specifika exemplet.
Låt oss försöka föreställa oss hur en personlig meddelandemodul kan se ut på ett system utvecklat av företaget som Johnnie flydde från. Och hur samma modul skulle se ut om utvecklare skulle tillämpa enhetstestning.
Modulen ska kunna lagra meddelandet i databasen och om personen som meddelandet var adresserat till finns i systemet — visa meddelandet på skärmen med en skålnotis.
//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); } } }
Låt oss kontrollera vilka beroenden vår modul har.
SendMessage-funktionen anropar statiska metoder för Notificationsservice- och Usersservice-objekten och skapar Messagesrepository-objektet som ansvarar för att arbeta med databasen.
Det är inga problem med att modulen interagerar med andra objekt. Problemet är hur den här interaktionen är uppbyggd, och den har inte byggts framgångsrikt. Direkt åtkomst till tredjepartsmetoder har gjort vår modul tätt kopplad till specifika implementeringar.
Denna interaktion har många nackdelar, men det viktiga är att Messagingservice-modulen har förlorat förmågan att testas isolerat från implementeringarna av Notificationsservice, Usersservice och Messagesrepository. Vi kan faktiskt inte ersätta dessa objekt med stubbar.
Låt oss nu titta på hur samma modul skulle se ut om en utvecklare skulle ta hand om den.
//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 är den här versionen mycket bättre. Interaktionen mellan objekt byggs nu inte direkt utan genom gränssnitt.
Vi behöver inte längre komma åt statiska klasser och instansiera objekt i metoder med affärslogik. Huvudpoängen är att vi kan ersätta alla beroenden genom att skicka in stubbar för testning till en konstruktor. Således, samtidigt som vi förbättrar kodtestbarheten, kan vi också förbättra både testbarheten för vår kod och arkitekturen för vår applikation. Vi vägrade direkt använda implementeringar och skickade instansieringen till lagret ovan. Det här är precis vad Johnnie ville ha.
Skapa sedan ett test för modulen för att skicka meddelanden.
Specifikation om tester
Definiera vad vårt test ska kontrollera:
- Ett enda anrop av metoden SaveMessage
- Ett enstaka anrop av metoden SendNotificationToUser() om metodstubben IsUserOnline() över IUsersService-objektet returnerar true
- Det finns ingen SendNotificationToUser()-metod om metodstubben IsUserOnline() över IUsersService-objektet returnerar false
Att följa dessa villkor kan garantera att implementeringen av SendMessage-meddelandet är korrekt och inte innehåller några fel.
Tester
Testet implementeras med hjälp av det isolerade Moq-ramverket
[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); }
För att sammanfatta det är att leta efter en idealisk arkitektur en värdelös uppgift.
Enhetstester är bra att använda när du behöver kontrollera arkitekturen vid förlorad koppling mellan moduler. Kom ändå ihåg att design av komplexa tekniska system alltid är en kompromiss. Det finns ingen idealisk arkitektur och det är inte möjligt att ta hänsyn till alla scenarier för applikationsutvecklingen i förväg. Arkitekturkvaliteten beror på flera parametrar, ofta uteslutande. Du kan lösa alla designproblem genom att lägga till en extra abstraktionsnivå. Den hänvisar dock inte till problemet med en enorm mängd abstraktionsnivåer. Jag rekommenderar inte att man tänker att interaktion mellan objekt enbart bygger på abstraktioner. Poängen är att du använder koden som tillåter interaktion mellan implementeringar och är mindre flexibel, vilket gör att den inte har möjlighet att testas med enhetstester.