web-dev-qa-db-fra.com

Tester par unité une classe qui appelle une méthode statique

J'essaie de tester une classe 'A' qui appelle une méthode statique d'une classe 'B'. La classe 'B' a essentiellement un cache Google guava qui récupère une valeur (Object) du cache à l'aide d'une clé ou charge l'objet dans le cache (en cas de manque de cache) à l'aide d'un adaptateur de service. La classe d'adaptateur de service a à son tour d'autres dépendances auto-câblées pour récupérer l'objet.

Ce sont les classes à des fins d'illustration:

Classe A

public class A {
    public Object getCachedObject(String key) {
        return B.getObjectFromCache(key);
    }
}

Classe B

public class B {

    private ServiceAdapter serviceAdapter;

    public void setServiceAdapter(ServiceAdapter serAdapt) {
        serviceAdapter = serAdapt;
    } 

    private static final LoadingCache<String, Object> CACHE = CacheBuilder.newBuilder()
                .maximumSize(100) 
                .expireAfterWrite(30, TimeUnit.MINUTES)
                .build(new MyCacheLoader());

    public static Object getObjectFromCache(final String key) throws ExecutionException {
        return CACHE.get(warehouseId);
    }

    private static class MyCacheLoader extends CacheLoader<String, Object>  {

        @Override
        public Object load(final String key) throws Exception {
            return serviceAdapter.getFromService(key)
        }
    }
}

Classe d'adaptateur de service

public class ServiceAdapter {
        @Autowired
        private MainService mainService

        public Object getFromService(String key) {
            return mainService.getTheObject(key);
        }
    }

Je suis capable de faire le test d'intégration avec succès et d'extraire (ou de charger) la valeur de (ou dans) le cache. Cependant, je ne parviens pas à écrire le test unitaire pour la classe A. Voici ce que j'ai essayé:

Test unitaire pour la classe A

@RunWith(EasyMocker.class)
public class ATest {
    private final static String key = "abc";
    @TestSubject
    private A classUnderTest = new A();

    @Test
    public void getCachedObject_Success() throws Exception {
        B.setServiceAdapter(new ServiceAdapter());
        Object expectedResponse = createExpectedResponse(); //some private method 
        expect(B.getObjectFromCache(key)).andReturn(expectedResponse).once();
        Object actualResponse = classUnderTest.getCachedObject(key);
        assertEquals(expectedResponse, actualResponse);
    }
}

Lorsque j'exécute le test unitaire, il échoue avec une exception NullPointerException à la classe ServiceAdapter où l'appel: mainService.getTheObject (clé) est effectué. 

Comment puis-je simuler la dépendance de ServiceAdapter lors du test unitaire de la classe A. Ne devrais-je pas simplement me préoccuper de la dépendance immédiate de la classe A, à savoir. B. 

Je suis sûr que je fais quelque chose de fondamentalement faux. Comment dois-je écrire le test unitaire pour la classe A?

4
user1639485

Vous savez maintenant pourquoi les méthodes statiques sont considérées comme une mauvaise pratique pour les tests unitaires, car elles rendent la moquerie presque impossible, en particulier. si ils sont stateful.

Il est donc plus pratique de reformuler les méthodes static de B en un ensemble de méthodes publiques non statiques.

La classe A devrait recevoir une instance de classe B injectée, soit via l'injection de constructeur ou l'injecteur. Dans votre test, vous instanciez ensuite la classe A avec une maquette de la classe B et faites-la retourner ce que vous voulez en fonction de votre scénario de test et basez vos assertions sur celle-ci.

En faisant cela, vous testez vraiment le unit , qui devrait en fin de compte être l’interface publique de la classe A. (C’est aussi pourquoi j’aime qu’une classe ait une seule méthode publique dans un monde idéal.)


En ce qui concerne votre exemple spécifique: La maquette de B ne devrait pas non plus se soucier de ses propres dépendances. Vous écrivez actuellement dans votre test:

 B.setServiceAdapter(new ServiceAdapter());       

Vous êtes dans ATest. Pas dans BTest. ATest ne devrait avoir qu'un mock of B, la transmission d'une instance de ServiceAdapter ne devrait donc pas être requise.

Vous ne devez vous soucier que du comportement des méthodes publiques de A et cela peut changer en fonction de certaines réponses des méthodes publiques de B.

Ce que je trouve aussi étrange, c’est que la méthode que vous voulez tester n’est qu’un wrapper pour B. C’est peut-être logique dans votre cas, mais cela me fait également penser que vous voulez peut-être déjà injecter un Object dans A au lieu d’une instance de B .

Si vous voulez ne pas vous perdre dans l'enfer moqueur, il est vraiment utile d'avoir autant de méthodes publiques par classe qui, à leur tour, ont le moins de dépendances possibles. Je m'efforce de créer trois dépendances par classe et j'autorise jusqu'à cinq pour des occasions spéciales. (Chaque dépendance peut avoir un impact énorme sur les frais généraux moqueurs.)

Si vous avez trop de dépendances, certaines parties peuvent certainement être déplacées vers d'autres/nouveaux services.

4
k0pernikus

Réécrire le code pour le rendre plus testable a déjà été expliqué dans une autre réponse. Parfois, il est difficile d'éviter ces cas.

Si vous voulez vraiment vous moquer d'un appel statique, vous pouvez utiliser PowerMock. Vous devrez utiliser @PrepareForTest ({CACHE.class}) pour votre classe, suivi du code ci-dessous dans le test unitaire.

      Object testObject = null; // Change this
      PowerMock.mockStatic(CACHE.class);
      expect(CACHE.get(anyString())).andReturn(testObject);
      PowerMock.replay(CACHE.class);
1
Andy

Afin de résoudre ce problème, vous pouvez envelopper une classe de type de référentiel interfacé autour de la classe B. Une fois que vous avez une interface, vous pouvez la remplacer à des fins de test.

En faisant cela, vous isolez A du fonctionnement interne de B et vous concentrez uniquement sur les actions résultantes de B (je suppose que ceci est simplement une autre façon de dire de programmer à une interface plutôt qu'à une classe concrète)

0
Gavin