web-dev-qa-db-fra.com

Utiliser Mockito pour tester des classes abstraites

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 framework moqueur (j'utilise Mockito) au lieu de fabriquer ma maquette à la main? Comment?

182
ripper234

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

utilisez Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS), puis simulez les méthodes abstraites 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());
    }
}

Remarque: L'intérêt de cette solution réside dans le fait que vous n'avez pas à implémenter les méthodes abstraites, tant qu'elles ne sont jamais appelées.

À mon avis, cela est préférable à l’utilisation d’un espion, puisqu’il nécessite une instance, ce qui signifie que vous devez créer une sous-classe instanciable de votre classe abstraite.

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

(Oui, c'est une conception moche, mais certains frameworks, par exemple Tapestry 4, vous le force en quelque sorte.)

La solution de contournement consiste à inverser cette approche - utilisez le comportement fictif habituel (c’est-à-dire que tout est simulé/stubbed) 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-vides, vous devrez utiliser thenCallRealMethod() à la place, par exemple:

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

Dans le cas contraire, Mockito se plaindra "Un stubbing inachevé a été détecté."

66
David Moles

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()));
16
Richard Nichols

Les frameworks moqueurs sont conçus pour faciliter la création de dépendances de la classe que vous testez. Lorsque vous utilisez un framework moqueur 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 permettant de détecter le moment où une méthode est appelée et de renvoyer une fausse valeur.

Lorsque vous testez une classe abstraite, vous souhaitez exécuter les méthodes non abstraites du sujet à tester (SUT), de sorte qu'un cadre moqueur n'est pas ce que vous voulez.

Une partie de la confusion provient du fait que la réponse à la question à laquelle vous vous êtes lié a consisté à créer à la main une maquette qui s'étend de votre classe abstraite. Je n'appellerais pas une telle classe une farce. Une maquette est une classe qui est utilisée pour remplacer 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 aboutit à trop de code, cela peut indiquer que votre classe est difficile à étendre.

Une autre solution consisterait à rendre votre scénario de test lui-même abstrait, avec une méthode abstraite pour créer le SUT (en d’autres termes, le scénario de test utiliserait le modèle Modèle, méthode design).

13
NamshubWriter

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 méthode réelle pour les méthodes concrètes.

8
Leopoldo Muller

Ce qui me fait vraiment mal à propos de me moquer des classes abstraites, c’est que ni le constructeur par défaut YourAbstractClass () n’est appelé (il manque super () dans mock) et il ne semble pas y avoir de moyen dans Mockito d’initialiser par défaut les propriétés fictives (par exemple, Propriétés de liste avec ArrayList ou LinkedList vide).

Ma classe abstraite (fondamentalement, le code source de la classe est généré) ne fournit PAS d'injection de générateur de dépendance pour les éléments de liste, ni de constructeur dans lequel 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; Private List dep2 = new ArrayList

Donc, il n'y a AUCUN moyen de se moquer d'une classe abstraite sans utiliser une implémentation d'objet réel (par exemple, une définition de classe interne dans une classe de test unitaire, redéfinissant les méthodes abstraites) et d'espionner l'objet réel (qui initialise correctement le champ).

Dommage que seul PowerMock puisse aider ici davantage.

5
Thomas Heiss

En supposant que vos classes de test se trouvent dans le même package (sous une racine source différente) que vos classes à tester, vous pouvez simplement créer le modèle:

YourClass yourObject = mock(YourClass.class);

et appelez les méthodes que vous souhaitez tester comme n'importe quelle autre méthode.

Vous devez définir des attentes pour chaque méthode appelée, ainsi que des méthodes concrètes appelant la méthode super - vous ne savez pas comment procéder avec Mockito, mais je pense que c'est possible avec EasyMock.

Tout ce que vous faites est de créer une instance concrète de YouClass et de vous éviter 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.

2
Nick Holt

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

private AbstractClassName classToTest;

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

// Test the AbstractClassName methods.
1
DwB

Whitebox.invokeMethod (..) peut être pratique dans ce cas.

0
Smart Coder

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

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.

0
Jorge Pastor

Vous pouvez instancier une classe anonyme, injecter vos modèles et ensuite 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);
    }
}

N'oubliez pas que la visibilité doit être protected pour la propriété myDependencyService de la classe abstraite ClassUnderTest.

0
Samuel