Ho deciso di scrivere questo articolo per mostrare che gli unit test non sono solo uno strumento per cimentarsi con la regressione nel codice, ma sono anche un ottimo investimento in un'architettura di alta qualità. Inoltre, un argomento nella comunità .NET inglese mi ha motivato a farlo. L'autore dell'articolo era Johnnie. Ha descritto il suo primo e l'ultimo giorno nell'azienda coinvolta nello sviluppo di software per le imprese nel settore finanziario. Johnnie stava facendo domanda per la posizione di sviluppatore di unit test. Era sconvolto dalla scarsa qualità del codice, che ha dovuto testare. Ha confrontato il codice con una discarica piena di oggetti che si clonano a vicenda in luoghi non adatti. Inoltre, non riusciva a trovare tipi di dati astratti in un repository:il codice conteneva solo il binding di implementazioni che si scambiavano richieste.
Johnnie, rendendosi conto di tutta l'inutilità dei test dei moduli in questa azienda, ha delineato questa situazione al manager, ha rifiutato di collaborare ulteriormente e ha dato un consiglio prezioso. Ha raccomandato a un team di sviluppo di seguire dei corsi per imparare a creare istanze di oggetti e utilizzare tipi di dati astratti. Non so se il gestore abbia seguito il suo consiglio (penso di no). Tuttavia, se sei interessato a cosa intendeva Johnnie e in che modo l'utilizzo del test dei moduli può influenzare la qualità della tua architettura, puoi leggere questo articolo.
L'isolamento delle dipendenze è una base per il test dei moduli
Il test del modulo o dell'unità è un test che verifica la funzionalità del modulo isolata dalle sue dipendenze. L'isolamento delle dipendenze è una sostituzione degli oggetti del mondo reale, con i quali interagisce il modulo in fase di test, con stub che simulano il comportamento corretto dei loro prototipi. Questa sostituzione permette di concentrarsi sul test di un particolare modulo, ignorando un possibile comportamento scorretto del suo ambiente. La necessità di sostituire le dipendenze nel test provoca una proprietà interessante. Uno sviluppatore che si rende conto che il proprio codice verrà utilizzato nei test dei moduli deve sviluppare utilizzando astrazioni ed eseguire il refactoring ai primi segnali di connettività elevata.
Lo considererò sull'esempio particolare.
Proviamo a immaginare come potrebbe essere un modulo di messaggio personale su un sistema sviluppato dall'azienda da cui Johnnie è scappato. E come sarebbe lo stesso modulo se gli sviluppatori applicassero lo unit test.
Il modulo dovrebbe essere in grado di memorizzare il messaggio nel database e, se la persona a cui è stato indirizzato il messaggio è nel sistema, visualizzare il messaggio sullo schermo con una notifica di brindisi.
//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); } } }
Verifichiamo quali dipendenze ha il nostro modulo.
La funzione SendMessage richiama i metodi statici degli oggetti Notificationsservice e Usersservice e crea l'oggetto Messagesrepository responsabile dell'utilizzo del database.
Non ci sono problemi con il fatto che il modulo interagisce con altri oggetti. Il problema è come viene costruita questa interazione e non viene costruita correttamente. L'accesso diretto a metodi di terze parti ha reso il nostro modulo strettamente collegato a implementazioni specifiche.
Questa interazione ha molti aspetti negativi, ma l'importante è che il modulo Messagingservice ha perso la capacità di essere testato in isolamento dalle implementazioni di Notificationsservice, Usersservice e Messagesrepository. In realtà, non possiamo sostituire questi oggetti con stub.
Ora diamo un'occhiata a come sarebbe lo stesso modulo se uno sviluppatore se ne occupasse.
//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); } } }
Come puoi vedere, questa versione è molto meglio. L'interazione tra oggetti ora è costruita non direttamente ma tramite interfacce.
Non è più necessario accedere a classi statiche e istanziare oggetti in metodi con logica aziendale. Il punto principale è che possiamo sostituire tutte le dipendenze passando gli stub per il test in un costruttore. Pertanto, migliorando la verificabilità del codice, potremmo anche migliorare sia la verificabilità del nostro codice che l'architettura della nostra applicazione. Ci siamo rifiutati di utilizzare direttamente le implementazioni e abbiamo passato l'istanza al livello sopra. Questo è esattamente ciò che voleva Johnnie.
Quindi, crea un test per il modulo di invio di messaggi.
Specifica sui test
Definisci cosa dovrebbe controllare il nostro test:
- Una singola chiamata del metodo SaveMessage
- Una singola chiamata del metodo SendNotificationToUser() se lo stub del metodo IsUserOnline() sull'oggetto IUsersService restituisce true
- Non esiste un metodo SendNotificationToUser() se lo stub del metodo IsUserOnline() sull'oggetto IUsersService restituisce false
Il rispetto di queste condizioni può garantire che l'implementazione del messaggio SendMessage sia corretta e non contenga errori.
Prove
Il test viene implementato utilizzando il framework Moq isolato
[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); }
Per riassumere, cercare un'architettura ideale è un compito inutile.
Gli unit test sono ottimi da utilizzare quando è necessario controllare l'architettura per perdere l'accoppiamento tra i moduli. Tuttavia, tieni presente che la progettazione di sistemi ingegneristici complessi è sempre un compromesso. Non esiste un'architettura ideale e non è possibile tenere conto a priori di tutti gli scenari di sviluppo dell'applicazione. La qualità dell'architettura dipende da più parametri, spesso mutuamente esclusivi. Puoi risolvere qualsiasi problema di progettazione aggiungendo un ulteriore livello di astrazione. Tuttavia, non si riferisce al problema di un'enorme quantità di livelli di astrazione. Non consiglio di pensare che l'interazione tra oggetti sia basata solo su astrazioni. Il punto è che usi il codice che consente l'interazione tra le implementazioni ed è meno flessibile, il che significa che non ha la possibilità di essere testato da unit test.