web-dev-qa-db-fra.com

Comment puis-je me moquer d'une méthode vide pour générer une exception?

J'ai une structure comme celle-ci:

public class CacheWrapper {
    private Map<Object, Object> innerMap;

    public CacheWrapper() {
        //initialize the innerMap with an instance for an in-memory cache
        //that works on external server
        //current implementation is not relevant for the problem
        innerMap = ...;
    }

    public void putInSharedMemory(Object key, Object value) {
        innerMap.put(key, value);
    }

    public Object getFromSharedMemory(Object key) {
        return innerMap.get(key);
    }
}

Et ma classe client (on pourrait dire que ça ressemble à ça):

public class SomeClient {
    //Logger here, for exception handling
    Logger log = ...;
    private CacheWrapper cacheWrapper;
    //getter and setter for cacheWrapper...

    public Entity getEntity(String param) {
        Entity someEntity = null;
        try {
            try {
                entity = cacheWrapper.getFromSharedMemory(param);
            } catch (Exception e) {
                //probably connection failure occurred here
                log.warn("There was a problem when getting from in-memory " + param + " key.", e);
            }
            if (entity == null) {
                entity = ...; //retrieve it from database
                //store in in-memory cache
                try {
                    cacheWrapper.put(param, entity);
                } catch (Exception e) {
                    //probably connection failure occurred here
                    log.warn("There was a problem when putting in in-memory " + param + " key.", e);
                }
            }
        } catch (Exception e) {
            logger.error(".......", e);
        }
        return entity;
    }
}

Je crée des tests unitaires pour la méthode SomeClient#getEntityet dois couvrir tous les scénarios. Par exemple, je dois couvrir le scénario dans lequel des exceptions sont générées par cacheWrapper. L'approche que je suis consiste à créer une maquette pour la classe CacheWrapper, à définir les méthodes de la classe CacheWrapper pour générer une RuntimeException, à définir cette maquette dans une instance de SomeClient et à tester Someclient#getEntity. Le problème est lorsque vous essayez de simuler la méthode putInSharedMemory car elle est void. J'ai essayé beaucoup de façons de le faire, mais aucune d'entre elles ne fonctionne. Le projet a des dépendances pour PowerMock et EasyMock .

Voici mes tentatives:

  1. Utiliser EasyMock.<Void>expect. Cela a provoqué une erreur du compilateur.
  2. J'ai essayé de remplacer CacheWrapper#putInSharedMemory. N'a pas fonctionné car une exception s'est produite avec ce message d'erreur:

    Java.lang.AssertionError: Appel de méthode inattendu putInSharedMemory ("foo", com.company.domain.Entity@609fc98)

  3. Ajout de la dépendance Mockito au projet pour utiliser les fonctionnalités de la classe PowerMockito. Mais cela a soulevé une exception car il n’intègre pas EasyMock. C'est l'exception levée:

    Exception Java.lang.ClassCast: org.powermock.api.easymock.internal.invocationcontrol.EasyMockMethodInvocationControl ne peut pas être converti en org.powermock.api.mockito.internal.invocationcontrol.MockitoMethodInvocationControl

Voici le code pour cet exemple de test unitaire:

@Test
public void getEntityWithCacheWrapperException() {
    CacheWrapper cacheWrapper = mockThrowsException();
    SomeClient someClient = new SomeClient();
    someClient.setCacheWrapper(cacheWrapper);
    Entity entity = someClient.getEntity();
    //here.....................^
    //cacheWrapper.putInSharedMemory should throw an exception

    //start asserting here...
}

//...

public CacheWrapper mockThrowsException() {
    CacheWrapper cacheWrapper = PowerMock.createMock(CacheWrapper.class);
    //mocking getFromSharedMemory method
    //this works like a charm
    EasyMock.expect(cacheWrapper.getFromSharedMemory(EasyMock.anyObject()))
        .andThrow(new RuntimeException("This is an intentional Exception")).anyTimes();

    //mocking putInSharedMemory method
    //the pieces of code here were not executed at the same time
    //instead they were commented and choose one approach after another

    //attempt 1: compiler exception: <Void> is not applicable for <void>
    EasyMock.<Void>expect(cacheWrapper.putInSharedMemory(EasyMock.anyObject(), EasyMock.anyObject()))
        .andThrow(new RuntimeException("This is an intentional Exception")).anyTimes();

    //attempt 2: stubbing the method
    //exception when executing the test:
     //Unexpected method call putInSharedMemory("foo", com.company.domain.Entity@609fc98)
    Method method = PowerMock.method(CacheWrapper.class, "putInSharedMemory", Object.class, Object.class);
    PowerMock.stub(method).toThrow(new RuntimeException("Exception on purpose."));

    //attempt 3: added dependency to Mockito integrated to PowerMock
    //bad idea: the mock created by PowerMock.createMock() belongs to EasyMock, not to Mockito
    //so it breaks when performing the when method
    //exception:
    //Java.lang.ClassCastException: org.powermock.api.easymock.internal.invocationcontrol.EasyMockMethodInvocationControl
    //cannot be cast to org.powermock.api.mockito.internal.invocationcontrol.MockitoMethodInvocationControl
    //at org.powermock.api.mockito.internal.expectation.PowerMockitoStubberImpl.when(PowerMockitoStubberImpl.Java:54)
    PowerMockito.doThrow(new RuntimeException("Exception on purpose."))
        .when(cacheWrapper).putInSharedMemory(EasyMock.anyObject(), EasyMock.anyObject());

    PowerMock.replay(cacheWrapper);
    return cacheWrapper;
}

Je ne peux pas modifier l'implémentation de CacheWrapper car elle provient d'une bibliothèque tierce. De plus, je ne peux pas utiliser EasyMock#getLastCall car j'effectue le test sur SomeClient#getEntity.

Comment puis-je surmonter cela?

5
Luiggi Mendoza

Étant donné qu'aucun de vos cours n'est final, vous pouvez utiliser "pure mockito" sans recourir à PowerMockito:

final CacheWrapper wrapper = Mockito.spy(new CacheWrapper());

Mockito.doThrow(something)
    .when(wrapper).putInSharedMemory(Matchers.any(), Matchers.any());

Notez que les "arguments de méthode" d'un stub sont en fait des correspondeurs d'arguments; vous pouvez mettre des valeurs spécifiques (s'il n'est pas "entouré" par une méthode spécifique, il appellera .equals()). Ainsi, vous pouvez guider le comportement du stub différemment pour différents arguments.

En outre, aucun besoin de .replay() avec Mockito, ce qui est très agréable!

Enfin, sachez que vous pouvez aussi doCallRealMethod(). Après cela, cela dépend de vos scénarios ...

(note: la dernière version de mockito disponible sur maven est la 1.10.17 FWIW)

12
fge

Utilisez-vous EasyMock ou Mockito? Les deux sont des cadres différents.

PowerMockito est un sur-ensemble (ou plus d'un supplément) qui peut être utilisé avec ces deux cadres. PowerMockito vous permet de faire des choses que Mockito ou EasyMock ne font pas.

Essayez ceci pour les méthodes de stubbing void pour générer des exceptions:

EasyMock:

// First make the actual call to the void method.
cacheWrapper.putInSharedMemory("key", "value");
EasyMock.expectLastCall().andThrow(new RuntimeException());

Vérifier:

Mockito:

 // Create a CacheWrapper spy and stub its method to throw an exception.
 // Syntax for stubbing a spy's method is different from stubbing a mock's method (check Mockito's docs).
 CacheWrapper spyCw = spy(new CacheWrapper()); 
 Mockito.doThrow(new RuntimeException())
     .when(spyCw)
     .putInSharedMemory(Matchers.any(), Matchers.any());

 SomeClient sc = new SomeClient();
 sc.setCacheWrapper(spyCw);

 // This will call spyCw#putInSharedMemory that will throw an exception.
 sc.getEntity("key");
3
nhylated

Utilisez expectLastCall , comme:

    cacheWrapper.putInSharedMemory(EasyMock.anyObject(), EasyMock.anyObject())
    EasyMock.expectLastCall().andThrow(new RuntimeException("This is an intentional Exception")).anyTimes();
0
NiematojakTomasz