2009-07-06 6 views
162

Je voudrais tester une classe abstraite. Bien sûr, je peux manually write a mock qui hérite de la classe. Est-ce que je peux faire ceci en utilisant un cadre moqueur (j'utilise Mockito) au lieu de fabriquer mon simulacre à la main? Comment?Utilisation de Mockito pour tester des classes abstraites

+2

Depuis Mockito [1.10.12] (http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html#30), Mockito supporte directement les classes abstraites d'espionnage/moquerie: 'SomeAbstract spy = spy (SomeAbstract.class); ' – pesche

Répondre

267

La suggestion suivante vous permet de tester des classes abstraites sans créer de sous-classe "réelle". Le bloc correspond à la sous-classe.

utilisez Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS), puis de simuler les méthodes abstraites appelé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 n'avez à mettre en œuvre les méthodes abstraites, tant qu'ils ne sont jamais invoquées. À mon avis, c'est plus facile que d'utiliser un espion, car un espion a besoin d'une instance, ce qui signifie que vous devez créer une sous-classe instanciable de votre classe abstraite.

+0

+1 pour m'aider à résoudre mon problème sans rapport avec la question. Le CALLS_REAL_METHODS était utile pour injecter un objet de bout dans mon SUT. Un talon avait plus de sens qu'un simulacre puisque les valeurs de retour étaient un peu complexes. – Snekse

+10

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. –

+6

Cela fonctionne réellement lorsque la classe abstraite appelle des méthodes abstraites. Utilisez simplement la syntaxe doReturn ou doNothing à la place de Mockito.when pour remplacer les méthodes abstraites, et si vous traitez des appels concrets, assurez-vous que l'écrasement des appels abstraits est prioritaire. –

2

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

YourClass yourObject = mock(YourClass.class); 

et appeler les méthodes que vous voulez tester comme vous serait une autre méthode.

Vous devez fournir des attentes pour chaque méthode appelée dans l'attente de toute méthode concrète appelant la super méthode - je ne sais pas comment vous feriez avec Mockito, mais je crois que c'est possible avec EasyMock. Tout ce que vous faites est de créer une instance concrète de YouClass et de vous épargner l'effort 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 de la fonctionnalité fournie par la classe abstraite.

+3

Mais l'utilisation de la maquette ne testera pas les méthodes concrètes de YourClass, ou ai-je tort? Ce n'est pas ce que je cherche. – ripper234

+1

C'est correct, ce qui précède ne fonctionnera pas si vous voulez appeler les méthodes concrètes sur la classe abstraite. –

+0

Excuses, je vais éditer le peu au sujet de l'attente, qui sont exigés pour chaque méthode que vous appelez non seulement les abrégés. –

13

Les cadres de simulation sont conçus pour simplifier l'élimination des dépendances de la classe que vous testez. Lorsque vous utilisez un cadre de simulation pour simuler une classe, la plupart des cadres 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 retourner une valeur fausse. Lorsque vous testez une classe abstraite, vous voulez exécuter les méthodes non abstraites de l'objet sous test (SUT), donc un cadre de simulation n'est pas ce que vous voulez.

Une partie de la confusion est que la réponse à la question que vous avez liée à dit de faire à la main une maquette qui s'étend de votre classe abstraite. Je n'appellerais pas une telle classe un simulacre. Un simulacre 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 indiquer que votre classe est difficile à étendre. Une solution alternative serait de rendre votre cas de test abstrait, avec une méthode abstraite pour créer le SUT (en d'autres termes, le cas de test utiliserait le motif de conception Template Method).

6

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 des méthodes abstraites et appeler la méthode réelle des méthodes concrètes.

15

Vous pouvez y parvenir en utilisant un espion (utilisez 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())); 
5

Ce qui me fait vraiment sentir mal à se moquer des classes abstraites est le fait que ni le constructeur par défaut YourAbstractClass() est appelée (manque super() dans la maquette), ni il semble y avoir une façon Mockito par défaut initialise les propriétés fantaisie (par exemple, liste les propriétés avec ArrayList vide ou LinkedList).

Ma classe abstraite (essentiellement le code source de classe généré) ne fournit PAS d'injection de set 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). Les attributs de classe utilisent l'initialisation par défaut: private Liste dep1 = new ArrayList; private Liste dep2 = new ArrayList

Il est donc INTERDIT de simuler une classe abstraite sans utiliser une implémentation d'objet réel (par exemple, définition de classe interne dans une classe de test unitaire, substitution de méthodes abstraites) et espionner l'objet réel initialisation de champ).

Dommage que seul PowerMock puisse aider ici davantage.

66

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

(Oui, c'est une conception moche, mais certains cadres, par exemple Tapestry 4, la force sorte de sur vous.)

La solution consiste à inverser cette approche - utiliser le comportement simulé ordinaire (c.-à-tout est mocked/stubbed) et utiliser 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(); 
    } 
} 

Mise à jour ajouter:

Pour les méthodes non-vides, vous aurez besoin d'utiliser thenCallRealMethod() à la place, par exemple:

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

Sinon Mockito se plaindra « Unfinished raclage détecté."

+6

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. –

+2

Oui, vous ne pouvez pas du tout compter sur l'état de l'objet, seulement le code de la méthode appelée. –

1

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

private AbstractClassName classToTest; 

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

// Test the AbstractClassName methods. 
0

Vous pouvez instancier une classe anonyme, injecter vos simulacres puis testez 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 protected pour la propriété myDependencyService de la classe abstraite ClassUnderTest.

Questions connexes