Mocks and Stubs - Comprendre les doubles de test avec Mockito

Une chose commune que je rencontre est que les équipes utilisant un cadre moqueur supposent qu'elles se moquent.

Ils ne savent pas que les simulacres ne sont que l'un des nombreux «tests doubles» que Gerard Meszaros a catégorisés sur xunitpatterns.com.

Il est important de réaliser que chaque type de test double a un rôle différent à jouer dans les tests. De la même manière que vous devez apprendre différents modèles ou refactoring, vous devez comprendre les rôles primitifs de chaque type de test double. Ceux-ci peuvent ensuite être combinés pour répondre à vos besoins de test.

Je vais couvrir un très bref historique de la façon dont cette classification est née, et comment chacun des types diffère.

Je vais le faire en utilisant quelques exemples courts et simples dans Mockito.

Pendant des années, les gens ont écrit des versions légères de composants système pour faciliter les tests. En général, cela s'appelait le stubbing. En 2000, l'article «Endo-Testing: Unit Testing with Mock Objects» a introduit le concept d'un Mock Object. Depuis lors, les stubs, les mocks et un certain nombre d'autres types d'objets de test ont été classés par Meszaros comme des doubles de test.

Cette terminologie a été référencée par Martin Fowler dans "Mocks Arn't Stubs" et est en cours d'adoption au sein de la communauté Microsoft, comme indiqué dans "Explorer le continuum des tests en double"

Un lien vers chacun de ces articles importants est indiqué dans la section de référence.

Le diagramme ci-dessus montre les types de test double couramment utilisés. L'URL suivante donne une bonne référence croisée à chacun des modèles et à leurs caractéristiques ainsi qu'à une terminologie alternative.

//xunitpatterns.com/Test%20Double.html

Mockito est un framework d'espionnage de test et il est très simple à apprendre. Il est à noter avec Mockito que les attentes des objets fictifs ne sont pas définies avant le test comme elles le sont parfois dans d'autres frameworks moqueurs. Cela conduit à un style plus naturel (IMHO) lorsque vous commencez à vous moquer.

Les exemples suivants sont ici purement pour donner une démonstration simple de l'utilisation de Mockito pour implémenter les différents types de tests doubles.

Il existe un nombre beaucoup plus grand d'exemples spécifiques d'utilisation de Mockito sur le site Web.

//docs.mockito.googlecode.com/hg/latest/org/mockito/Mockito.html

Voici quelques exemples de base utilisant Mockito pour montrer le rôle de chaque double test tel que défini par Meszaros.

J'ai inclus un lien vers la définition principale pour chacun afin que vous puissiez obtenir plus d'exemples et une définition complète.

//xunitpatterns.com/Dummy%20Object.html

C'est le plus simple de tous les doubles tests. C'est un objet qui n'a pas d'implémentation et qui est utilisé uniquement pour remplir les arguments des appels de méthode qui ne sont pas pertinents pour votre test.

Par exemple, le code ci-dessous utilise beaucoup de code pour créer le client, ce qui n'est pas important pour le test.

Le test ne se soucie pas moins du client ajouté, tant que le nombre de clients revient à un.

public Customer createDummyCustomer() { County county = new County("Essex"); City city = new City("Romford", county); Address address = new Address("1234 Bank Street", city); Customer customer = new Customer("john", "dobie", address); return customer; } @Test public void addCustomerTest() { Customer dummy = createDummyCustomer(); AddressBook addressBook = new AddressBook(); addressBook.addCustomer(dummy); assertEquals(1, addressBook.getNumberOfCustomers()); } 

En fait, nous ne nous soucions pas du contenu de l'objet client - mais c'est obligatoire. Nous pouvons essayer une valeur nulle, mais si le code est correct, vous vous attendez à ce qu'une sorte d'exception soit levée.

@Test(expected=Exception.class) public void addNullCustomerTest() { Customer dummy = null; AddressBook addressBook = new AddressBook(); addressBook.addCustomer(dummy); } 

Pour éviter cela, nous pouvons utiliser un simple mannequin Mockito pour obtenir le comportement souhaité.

@Test public void addCustomerWithDummyTest() { Customer dummy = mock(Customer.class); AddressBook addressBook = new AddressBook(); addressBook.addCustomer(dummy); Assert.assertEquals(1, addressBook.getNumberOfCustomers()); } 

C'est ce code simple qui crée un objet factice à passer dans l'appel.

Customer dummy = mock(Customer.class);

Ne vous laissez pas berner par la syntaxe simulée - le rôle joué ici est celui d'un mannequin, pas d'un simulacre.

C'est le rôle du double test qui le distingue, pas la syntaxe utilisée pour en créer un.

Cette classe fonctionne comme un simple substitut à la classe client et rend le test très facile à lire.

//xunitpatterns.com/Test%20Stub.html

Le rôle du stub de test est de renvoyer des valeurs contrôlées à l'objet testé. Celles-ci sont décrites comme des entrées indirectes du test. Espérons qu'un exemple clarifiera ce que cela signifie.

Prenez le code suivant

public class SimplePricingService implements PricingService { PricingRepository repository; public SimplePricingService(PricingRepository pricingRepository) { this.repository = pricingRepository; } @Override public Price priceTrade(Trade trade) { return repository.getPriceForTrade(trade); } @Override public Price getTotalPriceForTrades(Collection trades) { Price totalPrice = new Price(); for (Trade trade : trades) { Price tradePrice = repository.getPriceForTrade(trade); totalPrice = totalPrice.add(tradePrice); } return totalPrice; } 

Le SimplePricingService a un objet collaborateur qui est le référentiel central. Le référentiel central fournit les prix commerciaux au service de tarification via la méthode getPriceForTrade.

Pour que nous puissions tester la logique des businees dans le SimplePricingService, nous devons contrôler ces entrées indirectes

c'est-à-dire des entrées que nous n'avons jamais passées au test.

Ceci est illustré ci-dessous.

Dans l'exemple suivant, nous stub le PricingRepository pour renvoyer des valeurs connues qui peuvent être utilisées pour tester la logique métier de SimpleTradeService.

@Test public void testGetHighestPricedTrade() throws Exception { Price price1 = new Price(10); Price price2 = new Price(15); Price price3 = new Price(25); PricingRepository pricingRepository = mock(PricingRepository.class); when(pricingRepository.getPriceForTrade(any(Trade.class))) .thenReturn(price1, price2, price3); PricingService service = new SimplePricingService(pricingRepository); Price highestPrice = service.getHighestPricedTrade(getTrades()); assertEquals(price3.getAmount(), highestPrice.getAmount()); } 

Exemple de saboteur

Il existe 2 variantes courantes de talons de test: le répondeur et le saboteur.

Les répondeurs sont utilisés pour tester le chemin heureux comme dans l'exemple précédent.

Un saboteur est utilisé pour tester un comportement exceptionnel comme ci-dessous.

@Test(expected=TradeNotFoundException.class) public void testInvalidTrade() throws Exception { Trade trade = new FixtureHelper().getTrade(); TradeRepository tradeRepository = mock(TradeRepository.class); when(tradeRepository.getTradeById(anyLong())) .thenThrow(new TradeNotFoundException()); TradingService tradingService = new SimpleTradingService(tradeRepository); tradingService.getTradeById(trade.getId()); } 

//xunitpatterns.com/Mock%20Object.html

Les objets simulés sont utilisés pour vérifier le comportement des objets lors d'un test. Par comportement d'objet, je veux dire que nous vérifions que les méthodes et les chemins corrects sont exercés sur l'objet lorsque le test est exécuté.

Ceci est très différent du rôle de support d'un stub qui est utilisé pour fournir des résultats à tout ce que vous testez.

Dans un stub, nous utilisons le modèle de définition d'une valeur de retour pour une méthode.

when(customer.getSurname()).thenReturn(surname); 

Dans un simulacre, nous vérifions le comportement de l'objet en utilisant le formulaire suivant.

verify(listMock).add(s); 

Here is a simple example where we want to test that a new trade is audited correctly.

Here is the main code.

public class SimpleTradingService implements TradingService{ TradeRepository tradeRepository; AuditService auditService; public SimpleTradingService(TradeRepository tradeRepository, AuditService auditService) { this.tradeRepository = tradeRepository; this.auditService = auditService; } public Long createTrade(Trade trade) throws CreateTradeException { Long id = tradeRepository.createTrade(trade); auditService.logNewTrade(trade); return id; } 

The test below creates a stub for the trade repository and mock for the AuditService

We then call verify on the mocked AuditService to make sure that the TradeService calls it's

logNewTrade method correctly

@Mock TradeRepository tradeRepository; @Mock AuditService auditService; @Test public void testAuditLogEntryMadeForNewTrade() throws Exception { Trade trade = new Trade("Ref 1", "Description 1"); when(tradeRepository.createTrade(trade)).thenReturn(anyLong()); TradingService tradingService = new SimpleTradingService(tradeRepository, auditService); tradingService.createTrade(trade); verify(auditService).logNewTrade(trade); } 

The following line does the checking on the mocked AuditService.

verify(auditService).logNewTrade(trade);

This test allows us to show that the audit service behaves correctly when creating a trade.

//xunitpatterns.com/Test%20Spy.html

It's worth having a look at the above link for the strict definition of a Test Spy.

However in Mockito I like to use it to allow you to wrap a real object and then verify or modify it's behaviour to support your testing.

Here is an example were we check the standard behaviour of a List. Note that we can both verify that the add method is called and also assert that the item was added to the list.

@Spy List listSpy = new ArrayList(); @Test public void testSpyReturnsRealValues() throws Exception { String s = "dobie"; listSpy.add(new String(s)); verify(listSpy).add(s); assertEquals(1, listSpy.size()); } 

Compare this with using a mock object where only the method call can be validated. Because we only mock the behaviour of the list, it does not record that the item has been added and returns the default value of zero when we call the size() method.

@Mock List listMock = new ArrayList(); @Test public void testMockReturnsZero() throws Exception { String s = "dobie"; listMock.add(new String(s)); verify(listMock).add(s); assertEquals(0, listMock.size()); } 

Another useful feature of the testSpy is the ability to stub return calls. When this is done the object will behave as normal until the stubbed method is called.

In this example we stub the get method to always throw a RuntimeException. The rest of the behaviour remains the same.

@Test(expected=RuntimeException.class) public void testSpyReturnsStubbedValues() throws Exception { listSpy.add(new String("dobie")); assertEquals(1, listSpy.size()); when(listSpy.get(anyInt())).thenThrow(new RuntimeException()); listSpy.get(0); } 

In this example we again keep the core behaviour but change the size() method to return 1 initially and 5 for all subsequent calls.

public void testSpyReturnsStubbedValues2() throws Exception { int size = 5; when(listSpy.size()).thenReturn(1, size); int mockedListSize = listSpy.size(); assertEquals(1, mockedListSize); mockedListSize = listSpy.size(); assertEquals(5, mockedListSize); mockedListSize = listSpy.size(); assertEquals(5, mockedListSize); } 

This is pretty Magic!

//xunitpatterns.com/Fake%20Object.html

Les faux objets sont généralement fabriqués à la main ou des objets légers utilisés uniquement pour les tests et ne conviennent pas à la production. Un bon exemple serait une base de données en mémoire ou une fausse couche de service.

Ils ont tendance à fournir beaucoup plus de fonctionnalités que les doubles de test standard et, en tant que tels, ne sont probablement généralement pas candidats à la mise en œuvre à l'aide de Mockito. Cela ne veut pas dire qu'ils ne pourraient pas être construits comme tels, mais que cela ne vaut probablement pas la peine d'être implémenté de cette façon.

Tester les motifs doubles

Test endo: test unitaire avec des objets simulés

Des rôles simulés, pas des objets

Les mocs ne sont pas des talons

//msdn.microsoft.com/en-us/magazine/cc163358.aspx

Cette histoire, "Mocks And Stubs - Understanding Test Doubles With Mockito" a été initialement publiée par JavaWorld.