Utilisation de Mockito pour tester des classes abstraites

213

Je voudrais tester une classe abstraite. Bien sûr, je peux écrire manuellement une maquette qui hérite de la classe.

Puis-je faire cela en utilisant un cadre de simulation (j'utilise Mockito) au lieu de fabriquer ma maquette à la main? Comment?

ripper234
la source
2
Depuis Mockito 1.10.12 , Mockito prend en charge les classes abstraites d'espionnage / moquerie directement:SomeAbstract spy = spy(SomeAbstract.class);
pesche
6
Depuis Mockito 2.7.14, vous pouvez également vous moquer des classes abstraites qui nécessitent des arguments de constructeur viamock(MyAbstractClass.class, withSettings().useConstructor(arg1, arg2).defaultAnswer(CALLS_REAL_METHODS))
Gediminas Rimsa

Réponses:

315

La suggestion suivante vous permet de tester des classes abstraites sans créer une "vraie" sous-classe - le Mock est la sous-classe.

utiliser Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS), puis se moquer de toutes les méthodes abstraites qui sont invoquées.

Exemple:

public abstract class My {
  public Result methodUnderTest() { ... }
  protected abstract void methodIDontCareAbout();
}

public class MyTest {
    @Test
    public void shouldFailOnNullIdentifiers() {
        My my = Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS);
        Assert.assertSomething(my.methodUnderTest());
    }
}

Note: La beauté de cette solution est que vous ne devez mettre en œuvre les méthodes abstraites, tant qu'ils ne sont jamais invoquées.

À mon avis, c'est plus simple que d'utiliser un espion, car un espion nécessite une instance, ce qui signifie que vous devez créer une sous-classe instanciable de votre classe abstraite.

Morten Lauritsen Khodabocus
la source
14
Comme indiqué ci-dessous, cela ne fonctionne pas lorsque la classe abstraite appelle des méthodes abstraites pour être testée, ce qui est souvent le cas.
Richard Nichols
11
Cela fonctionne réellement lorsque la classe abstraite appelle des méthodes abstraites. Utilisez simplement la syntaxe doReturn ou doNothing au lieu de Mockito.when pour écraser les méthodes abstraites, et si vous arrêtez des appels concrets, assurez-vous que l'écrêtage des appels abstraits vient en premier.
Gonen I
2
Comment puis-je injecter des dépendances dans ce type d'objet (classe abstraite moquée appelant des méthodes réelles)?
Samuel
2
Cela se comporte de manière inattendue si la classe en question a des initialiseurs d'instance. Mockito ignore les initialiseurs pour les mocks, ce qui signifie que les variables d'instance qui sont initialisées en ligne seront inopinément nulles, ce qui peut provoquer des NPE.
digitalbath
1
Que faire si le constructeur de classe abstraite prend un ou plusieurs paramètres?
SD
68

Si vous avez juste besoin de tester certaines des méthodes concrètes sans toucher à aucun des résumés, vous pouvez utiliser CALLS_REAL_METHODS(voir la réponse de Morten ), mais si la méthode concrète testée appelle certains des résumés ou des méthodes d'interface non implémentées, cela ne fonctionnera pas - Mockito se plaindra "Impossible d'appeler la méthode réelle sur l'interface java."

(Oui, c'est un design moche, mais certains frameworks, par exemple Tapestry 4, le forcent sur vous.)

La solution de contournement consiste à inverser cette approche - utilisez le comportement de simulation ordinaire (c'est-à-dire que tout est moqué / tronqué) et utilisez doCallRealMethod()pour appeler explicitement la méthode concrète testée. Par exemple

public abstract class MyClass {
    @SomeDependencyInjectionOrSomething
    public abstract MyDependency getDependency();

    public void myMethod() {
        MyDependency dep = getDependency();
        dep.doSomething();
    }
}

public class MyClassTest {
    @Test
    public void myMethodDoesSomethingWithDependency() {
        MyDependency theDependency = mock(MyDependency.class);

        MyClass myInstance = mock(MyClass.class);

        // can't do this with CALLS_REAL_METHODS
        when(myInstance.getDependency()).thenReturn(theDependency);

        doCallRealMethod().when(myInstance).myMethod();
        myInstance.myMethod();

        verify(theDependency, times(1)).doSomething();
    }
}

Mis à jour pour ajouter:

Pour les méthodes non nulles, vous devrez utiliser à la thenCallRealMethod()place, par exemple:

when(myInstance.myNonVoidMethod(someArgument)).thenCallRealMethod();

Sinon, Mockito se plaindra "Détection de stubbing inachevé".

David Moles
la source
9
Cela fonctionnera dans certains cas, cependant Mockito n'appelle pas le constructeur de la classe abstraite sous-jacente avec cette méthode. Cela peut entraîner l'échec de la «méthode réelle» en raison de la création d'un scénario inattendu. Ainsi, cette méthode ne fonctionnera pas dans tous les cas non plus.
Richard Nichols
3
Oui, vous ne pouvez pas du tout compter sur l'état de l'objet, seul le code de la méthode est appelée.
David Moles
Oh donc les méthodes objet sont séparées de l'état, super.
haelix
17

Vous pouvez y parvenir en utilisant un espion (utilisez cependant la dernière version de Mockito 1.8+).

public abstract class MyAbstract {
  public String concrete() {
    return abstractMethod();
  }
  public abstract String abstractMethod();
}

public class MyAbstractImpl extends MyAbstract {
  public String abstractMethod() {
    return null;
  }
}

// your test code below

MyAbstractImpl abstractImpl = spy(new MyAbstractImpl());
doReturn("Blah").when(abstractImpl).abstractMethod();
assertTrue("Blah".equals(abstractImpl.concrete()));
Richard Nichols
la source
14

Les frameworks de simulation sont conçus pour faciliter la simulation des dépendances de la classe que vous testez. Lorsque vous utilisez un framework de simulation pour simuler une classe, la plupart des frameworks créent dynamiquement une sous-classe et remplacent l'implémentation de la méthode par du code pour détecter le moment où une méthode est appelée et renvoyer une fausse valeur.

Lorsque vous testez une classe abstraite, vous souhaitez exécuter les méthodes non abstraites du sujet en cours de test (SUT), donc un cadre de simulation n'est pas ce que vous voulez.

Une partie de la confusion réside dans le fait que la réponse à la question à laquelle vous avez lié a dit de fabriquer à la main une maquette qui s'étend de votre classe abstraite. Je n'appellerais pas une telle classe un simulacre. Une maquette est une classe qui est utilisée en remplacement d'une dépendance, est programmée avec des attentes et peut être interrogée pour voir si ces attentes sont satisfaites.

Au lieu de cela, je suggère de définir une sous-classe non abstraite de votre classe abstraite dans votre test. Si cela entraîne trop de code, cela peut être un signe que votre classe est difficile à étendre.

Une solution alternative serait de rendre votre cas de test lui-même abstrait, avec une méthode abstraite pour créer le SUT (en d'autres termes, le cas de test utiliserait le modèle de conception de la méthode de modèle).

NamshubWriter
la source
8

Essayez d'utiliser une réponse personnalisée.

Par exemple:

import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class CustomAnswer implements Answer<Object> {

    public Object answer(InvocationOnMock invocation) throws Throwable {

        Answer<Object> answer = null;

        if (isAbstract(invocation.getMethod().getModifiers())) {

            answer = Mockito.RETURNS_DEFAULTS;

        } else {

            answer = Mockito.CALLS_REAL_METHODS;
        }

        return answer.answer(invocation);
    }
}

Il retournera la maquette pour les méthodes abstraites et appellera la vraie méthode pour les méthodes concrètes.


la source
5

Ce qui me fait vraiment du mal à me moquer des classes abstraites, c'est le fait que ni le constructeur par défaut YourAbstractClass () n'est appelé (il manque super () dans mock) ni qu'il n'y a aucun moyen dans Mockito d'initialiser par défaut les propriétés fictives (par exemple, les propriétés List avec ArrayList ou LinkedList vide).

Ma classe abstraite (essentiellement le code source de la classe est générée) ne fournit PAS d'injection de définition de dépendance pour les éléments de liste, ni de constructeur où elle initialise les éléments de liste (que j'ai essayé d'ajouter manuellement).

Seuls les attributs de classe utilisent l'initialisation par défaut: private List dep1 = new ArrayList; Liste privée dep2 = new ArrayList

Il n'y a donc AUCUN moyen de se moquer d'une classe abstraite sans utiliser une implémentation d'objet réel (par exemple la définition de classe interne dans la classe de test unitaire, les méthodes abstraites écrasantes) et l'espionnage de l'objet réel (qui effectue l'initialisation correcte du champ).

Dommage que seul PowerMock puisse aider encore plus.

Thomas Heiss
la source
2

En supposant que vos classes de test sont dans le même package (sous une racine source différente) que vos classes sous test, vous pouvez simplement créer la maquette:

YourClass yourObject = mock(YourClass.class);

et appelez les méthodes que vous souhaitez tester comme vous le feriez pour toute autre méthode.

Vous devez fournir des attentes pour chaque méthode appelée avec l'attente de toute méthode concrète appelant la super méthode - je ne sais pas comment vous le feriez avec Mockito, mais je crois que c'est possible avec EasyMock.

Tout cela crée une instance concrète YouClasset vous évite de fournir des implémentations vides de chaque méthode abstraite.

En passant, je trouve souvent utile d'implémenter la classe abstraite dans mon test, où elle sert d'exemple d'implémentation que je teste via son interface publique, bien que cela dépende des fonctionnalités fournies par la classe abstraite.

Nick Holt
la source
3
Mais l'utilisation de la simulation ne testera pas les méthodes concrètes de YourClass, ou je me trompe? Ce n'est pas ce que je recherche.
ripper234
1
C'est vrai, ce qui précède ne fonctionnera pas si vous voulez invoquer les méthodes concrètes sur la classe abstraite.
Richard Nichols
Toutes mes excuses, je vais modifier le bit sur l'attente, qui est requis pour chaque méthode que vous appelez non seulement les méthodes abstraites.
Nick Holt le
mais vous testez toujours votre maquette, pas les méthodes concrètes.
Jonatan Cloutier
2

Vous pouvez étendre la classe abstraite avec une classe anonyme dans votre test. Par exemple (en utilisant Junit 4):

private AbstractClassName classToTest;

@Before
public void preTestSetup()
{
    classToTest = new AbstractClassName() { };
}

// Test the AbstractClassName methods.
DwB
la source
2

Mockito permet de se moquer des classes abstraites au moyen de l' @Mockannotation:

public abstract class My {

    public abstract boolean myAbstractMethod();

    public void myNonAbstractMethod() {
        // ...
    }
}

@RunWith(MockitoJUnitRunner.class)
public class MyTest {

    @Mock(answer = Answers.CALLS_REAL_METHODS)
    private My my;

    @Test
    private void shouldPass() {
        BDDMockito.given(my.myAbstractMethod()).willReturn(true);
        my.myNonAbstractMethod();
        // ...
    }
}

L'inconvénient est qu'il ne peut pas être utilisé si vous avez besoin de paramètres de constructeur.

Jorge Pastor
la source
0

Vous pouvez instancier une classe anonyme, injecter vos simulacres puis tester cette classe.

@RunWith(MockitoJUnitRunner.class)
public class ClassUnderTest_Test {

    private ClassUnderTest classUnderTest;

    @Mock
    MyDependencyService myDependencyService;

    @Before
    public void setUp() throws Exception {
        this.classUnderTest = getInstance();
    }

    private ClassUnderTest getInstance() {
        return new ClassUnderTest() {

            private ClassUnderTest init(
                    MyDependencyService myDependencyService
            ) {
                this.myDependencyService = myDependencyService;
                return this;
            }

            @Override
            protected void myMethodToTest() {
                return super.myMethodToTest();
            }
        }.init(myDependencyService);
    }
}

Gardez à l'esprit que la visibilité doit être protectedpour la propriété myDependencyServicede la classe abstraite ClassUnderTest.

Samuel
la source
0

Les PowerMock Whitebox.invokeMethod(..)peuvent être utiles dans ce cas.

Codeur intelligent
la source