Utilisez Mockito pour vous moquer de certaines méthodes mais pas d'autres

402

Existe-t-il un moyen, en utilisant Mockito, de se moquer de certaines méthodes dans une classe, mais pas d'autres?

Par exemple, dans ce ( il est vrai pièce ) Stockclasse je veux railler les getPrice()et les getQuantity()valeurs retour (comme indiqué dans l'extrait de test ci - dessous) , mais je veux que le getValue()pour effectuer la multiplication comme code dans la Stockclasse

public class Stock {
  private final double price;
  private final int quantity;

  Stock(double price, int quantity) {
    this.price = price;
    this.quantity = quantity;
  }

  public double getPrice() {
    return price;
  }

  public int getQuantity() {
    return quantity;
  }
  public double getValue() {
    return getPrice() * getQuantity();
  }

  @Test
  public void getValueTest() {
    Stock stock = mock(Stock.class);
    when(stock.getPrice()).thenReturn(100.00);
    when(stock.getQuantity()).thenReturn(200);
    double value = stock.getValue();
    // Unfortunately the following assert fails, because the mock Stock getValue() method does not perform the Stock.getValue() calculation code.
    assertEquals("Stock value not correct", 100.00*200, value, .00001);
}
Victor Grazi
la source
4
Pourquoi voudriez-vous faire ça? Vous devez soit tester la classe (dans ce cas, il ne devrait pas y avoir de moquerie du tout), soit la simuler pendant le test d'une classe différente (auquel cas, aucune fonctionnalité). Pourquoi feriez-vous une simulation partielle?
weltraumpirat
3
Ok, c'est un petit exemple de la vraie chose. En réalité, j'essaye d'éviter un appel à la base de données, en passant des valeurs artificielles, mais je veux vérifier que les autres méthodes fonctionnent correctement avec ces valeurs artificielles. Y a-t-il une meilleure manière de faire cela?
Victor Grazi
5
Certainement: déplacez vos appels de base de données vers une classe distincte (la logique du domaine et l'accès à la base de données ne doivent pas être dans la même classe; ce sont deux préoccupations différentes), extrayez son interface, utilisez cette interface pour vous connecter à partir de la classe logique du domaine et simulez uniquement le interface pendant les tests.
weltraumpirat
1
Je suis tout à fait d'accord, il est difficile d'expliquer l'ensemble de l'image sans télécharger des quantités de code ici, y compris des bibliothèques tierces.
Victor Grazi
1
Vous pourriez probablement. Mais alors, ce ne serait pas "une meilleure façon de le faire": votre code de base de données est un détail d'implémentation que vous souhaitez cacher au reste de votre application, probablement même passer à un autre package. Vous ne voudriez pas avoir à recompiler la logique de votre domaine à chaque fois que vous modifiez une déclaration de suite, n'est-ce pas?
weltraumpirat

Réponses:

644

Pour répondre directement à votre question, oui, vous pouvez vous moquer de certaines méthodes sans en moquer d'autres. C'est ce qu'on appelle une simulation partielle . Voir la documentation Mockito sur les simulations partielles pour plus d'informations.

Pour votre exemple, vous pouvez faire quelque chose comme ceci dans votre test:

Stock stock = mock(Stock.class);
when(stock.getPrice()).thenReturn(100.00);    // Mock implementation
when(stock.getQuantity()).thenReturn(200);    // Mock implementation
when(stock.getValue()).thenCallRealMethod();  // Real implementation

Dans ce cas, chaque implémentation de méthode est simulée, sauf indication contraire thenCallRealMethod() dans la when(..)clause.

Il y a aussi une possibilité dans l'autre sens avec espion au lieu de se moquer :

Stock stock = spy(Stock.class);
when(stock.getPrice()).thenReturn(100.00);    // Mock implementation
when(stock.getQuantity()).thenReturn(200);    // Mock implementation
// All other method call will use the real implementations

Dans ce cas, toutes les implémentations de méthodes sont les vraies, sauf si vous avez défini un comportement simulé avec when(..).

Il y a un écueil important lorsque vous utilisez when(Object)avec espion comme dans l'exemple précédent. La vraie méthode sera appelée (car elle stock.getPrice()est évaluée avant when(..)lors de l'exécution). Cela peut être un problème si votre méthode contient une logique qui ne doit pas être appelée. Vous pouvez écrire l'exemple précédent comme ceci:

Stock stock = spy(Stock.class);
doReturn(100.00).when(stock).getPrice();    // Mock implementation
doReturn(200).when(stock).getQuantity();    // Mock implementation
// All other method call will use the real implementations

Une autre possibilité peut être d'utiliser org.mockito.Mockito.CALLS_REAL_METHODS, comme:

Stock MOCK_STOCK = Mockito.mock( Stock.class, CALLS_REAL_METHODS );

Cela délègue des appels sans entrave à des implémentations réelles.


Cependant, avec votre exemple, je crois que ce sera toujours pas, depuis la mise en œuvre getValue()repose sur quantityet price, plutôt quegetQuantity() et getPrice(), ce dont vous vous êtes moqué.

Une autre possibilité consiste à éviter complètement les moqueries:

@Test
public void getValueTest() {
    Stock stock = new Stock(100.00, 200);
    double value = stock.getValue();
    assertEquals("Stock value not correct", 100.00*200, value, .00001);
}
Jon Newmuis
la source
21
Je pense que cette réponse est fausse. Vous devez ESPIONNER une instance de l'objet, pas MOCKER la classe.
GaRRaPeTa
2
@GaRRaPeTa Je dirais que l'espionnage et la moquerie sont deux alternatives raisonnables. Il est difficile de dire quel est le meilleur pour ce cas, car le PO indique qu'il s'agit d'un exemple simplifié.
Jon Newmuis
1
Ne devrait-il pas s'agir de "Spy" au lieu de "Mock", car la cabine de moquerie partielle devrait être mieux fournie par "Spy".
Tarun Sapra
2
Stock stock = spy(Stock.class);Cela semble faux, la spyméthode semble accepter uniquement les objets et non les classes.
Paramvir Singh Karwal
4
+1 pour avoir signalé la différence entre doReturn(retval).when(spyObj).methodName(args)etwhen(spyObj.methodName(args)).thenReturn(retval)
Captain_Obvious
140

La moquerie partielle d'une classe est également prise en charge via Spy in mockito

List list = new LinkedList();
List spy = spy(list);

//optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);

//using the spy calls real methods
spy.add("one");
spy.add("two");

//size() method was stubbed - 100 is printed
System.out.println(spy.size());

Consultez les documents 1.10.19et 2.7.22pour une explication détaillée.

Sudarshan
la source
37

Selon les documents :

Foo mock = mock(Foo.class, CALLS_REAL_METHODS);

// this calls the real implementation of Foo.getSomething()
value = mock.getSomething();

when(mock.getSomething()).thenReturn(fakeValue);

// now fakeValue is returned
value = mock.getSomething();
ema
la source
2
Merci d'avoir montré comment configurer une maquette où l'implémentation réelle est appelée pour toutes les méthodes, à l'exception des quelques-unes que je dois contrôler à partir du test.
bigh_29
class NaughtyLinkedList extends LinkedList { public int size() { throw new RuntimeException("don't call me");} } @Test public void partialMockNaughtLinkedList(){ List mock = mock(NaughtyLinkedList.class, CALLS_REAL_METHODS); mock.add(new Object()); // this calls the real function when(mock.size()).thenReturn(2); // For whatever reason, this lines throws the RuntimeException. assertEquals(2,mock.size()); }Ça ne marche pas. Pour quelque raison que ce soit, quand "quand" est exécuté, il exécute en fait la méthode qui est censée se moquer. Code:
Lance Kind
3
Le problème est "quand". Le «quand» exécutera en fait la chose que vous voulez partiellement moquer. Pour éviter cela, il existe une alternative: doReturn (). Voir le doReturn () sur docs.mockito.googlecode.com/hg/1.9.5/org/mockito/…
Lance Kind
18

Ce que vous voulez, c'est org.mockito.Mockito.CALLS_REAL_METHODSselon les documents:

/**
 * Optional <code>Answer</code> to be used with {@link Mockito#mock(Class, Answer)}
 * <p>
 * {@link Answer} can be used to define the return values of unstubbed invocations.
 * <p>
 * This implementation can be helpful when working with legacy code.
 * When this implementation is used, unstubbed methods will delegate to the real implementation.
 * This is a way to create a partial mock object that calls real methods by default.
 * <p>
 * As usual you are going to read <b>the partial mock warning</b>:
 * Object oriented programming is more less tackling complexity by dividing the complexity into separate, specific, SRPy objects.
 * How does partial mock fit into this paradigm? Well, it just doesn't... 
 * Partial mock usually means that the complexity has been moved to a different method on the same object.
 * In most cases, this is not the way you want to design your application.
 * <p>
 * However, there are rare cases when partial mocks come handy: 
 * dealing with code you cannot change easily (3rd party interfaces, interim refactoring of legacy code etc.)
 * However, I wouldn't use partial mocks for new, test-driven & well-designed code.
 * <p>
 * Example:
 * <pre class="code"><code class="java">
 * Foo mock = mock(Foo.class, CALLS_REAL_METHODS);
 *
 * // this calls the real implementation of Foo.getSomething()
 * value = mock.getSomething();
 *
 * when(mock.getSomething()).thenReturn(fakeValue);
 *
 * // now fakeValue is returned
 * value = mock.getSomething();
 * </code></pre>
 */

Ainsi, votre code devrait ressembler à:

import org.junit.Test;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

public class StockTest {

    public class Stock {
        private final double price;
        private final int quantity;

        Stock(double price, int quantity) {
            this.price = price;
            this.quantity = quantity;
        }

        public double getPrice() {
            return price;
        }

        public int getQuantity() {
            return quantity;
        }

        public double getValue() {
            return getPrice() * getQuantity();
        }
    }

    @Test
    public void getValueTest() {
        Stock stock = mock(Stock.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
        when(stock.getPrice()).thenReturn(100.00);
        when(stock.getQuantity()).thenReturn(200);
        double value = stock.getValue();

        assertEquals("Stock value not correct", 100.00 * 200, value, .00001);
    }
}

L'appel aux Stock stock = mock(Stock.class);appels org.mockito.Mockito.mock(Class<T>)qui ressemble à ceci:

 public static <T> T mock(Class<T> classToMock) {
    return mock(classToMock, withSettings().defaultAnswer(RETURNS_DEFAULTS));
}

Les documents de la valeur RETURNS_DEFAULTSindiquent:

/**
 * The default <code>Answer</code> of every mock <b>if</b> the mock was not stubbed.
 * Typically it just returns some empty value. 
 * <p>
 * {@link Answer} can be used to define the return values of unstubbed invocations. 
 * <p>
 * This implementation first tries the global configuration. 
 * If there is no global configuration then it uses {@link ReturnsEmptyValues} (returns zeros, empty collections, nulls, etc.)
 */
le mec
la source
1
Bien repéré ... mais je peux juste demander pourquoi vous utilisez withSettings()...comme ça? Il semble que org.mockito.internal.stubbing.answers.CallsRealMethods()(par exemple) pourrait faire le travail ... et le javadoc pour cette classe indique spécifiquement qu'il est destiné à être utilisé pour les moqueries partielles ...
Mike Rodent
3
De plus ... cela ne rencontrera-t-il pas le problème rencontré par d'autres réponses ici: c'est-à thenReturn- dire qu'il exécutera réellement la méthode (ce qui pourrait causer des problèmes, mais pas dans cet exemple), et doReturnest donc préférable dans un tel cas ...?
mike rodent
4

La moquerie partielle utilisant la méthode d'espionnage de Mockito pourrait être la solution à votre problème, comme déjà indiqué dans les réponses ci-dessus. Dans une certaine mesure, je conviens que, pour votre cas d'utilisation concret, il peut être plus approprié de se moquer de la recherche de base de données. D'après mon expérience, cela n'est pas toujours possible - du moins pas sans autres solutions de contournement - que je considérerais comme très lourd ou du moins fragile. Notez que le mocking partiel ne fonctionne pas avec les versions alliées de Mockito. Vous avez utilisé au moins 1.8.0.

J'aurais juste écrit un simple commentaire pour la question d'origine au lieu de poster cette réponse, mais StackOverflow ne le permet pas.

Encore une chose: je ne peux vraiment pas comprendre que plusieurs fois une question posée ici obtient un commentaire avec "Pourquoi vous voulez faire cela" sans au moins essayer de comprendre le problème. Surtout quand il s'agit de besoin de moqueries partielles, il y a vraiment beaucoup de cas d'utilisation que je pourrais imaginer où cela serait utile. C'est pourquoi les gars de Mockito ont fourni cette fonctionnalité. Cette fonctionnalité ne doit bien sûr pas être surutilisée. Mais lorsque nous parlons de configurations de cas de test qui autrement ne pourraient pas être établies de manière très compliquée, l'espionnage devrait être utilisé.

kflGalore
la source
2
Je pense que cette réponse est partiellement une opinion. Veuillez envisager de modifier.
sonslikeodd
2
A voté pour égayer le nouveau membre de la famille. Pas besoin d'avoir cette zone inve, rien de vraiment techniquement mauvais là-bas ou de langue / ton incorrect. Soyez gentil avec les nouveaux membres. Merci.
Saurabh Patil