Usa Mockito para burlarte de algunos métodos pero no de otros

402

¿Hay alguna forma, usando Mockito, para burlarse de algunos métodos en una clase, pero no de otros?

Por ejemplo, en esta Stockclase (ciertamente inventada) quiero burlarme de los valores getPrice()y getQuantity()devolverlos (como se muestra en el fragmento de prueba a continuación), pero quiero getValue()que realice la multiplicación codificada en la Stockclase

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
fuente
44
¿Por qué querrías hacer eso? Debería probar la clase (en cuyo caso, no debería haber ninguna burla) o debería burlarse mientras prueba una clase diferente (en cuyo caso, no hay funcionalidad). ¿Por qué harías un simulacro parcial?
weltraumpirat
3
Ok, este es un pequeño ejemplo de lo real. En realidad, estoy tratando de evitar una llamada a la base de datos, pasando valores artificiales, pero quiero verificar que los otros métodos funcionen correctamente con esos valores artificiales. ¿Hay una mejor manera de hacer esto?
Victor Grazi
55
Ciertamente: mueva las llamadas de su base de datos a una clase separada (la lógica de dominio y el acceso a la base de datos no deben estar en la misma clase; son dos preocupaciones diferentes), extraiga su interfaz, use esa interfaz para conectarse desde la clase de lógica de dominio y solo se burle de interfaz durante las pruebas.
weltraumpirat
1
Estoy completamente de acuerdo, es difícil explicar la imagen completa sin cargar grandes cantidades de código aquí, incluidas las bibliotecas de terceros.
Victor Grazi
1
Probablemente puedas. Pero entonces, esa no sería "una mejor manera de hacerlo": el código de su base de datos es un detalle de implementación que desea ocultar del resto de su aplicación, probablemente incluso pasar a un paquete diferente. No querrás tener que volver a compilar la lógica de tu dominio cada vez que cambies una declaración posterior, ¿verdad?
weltraumpirat

Respuestas:

644

Para responder directamente a su pregunta, sí, puede burlarse de algunos métodos sin burlarse de otros. Esto se llama simulacro parcial . Consulte la documentación de Mockito sobre simulacros parciales para obtener más información.

Para su ejemplo, puede hacer algo como lo siguiente, en su prueba:

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

En ese caso, se burla de la implementación de cada método, a menos que se especifique thenCallRealMethod()en la when(..)cláusula.

También hay una posibilidad al revés con espía en lugar de simulacro :

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

En ese caso, toda la implementación del método es la real, excepto si ha definido un comportamiento simulado con when(..).

Hay un obstáculo importante cuando se usa when(Object)con espía como en el ejemplo anterior. Se llamará al método real (porque stock.getPrice()se evalúa antes when(..)en tiempo de ejecución). Esto puede ser un problema si su método contiene lógica a la que no debería llamarse. Puedes escribir el ejemplo anterior así:

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

Otra posibilidad puede ser usar org.mockito.Mockito.CALLS_REAL_METHODS, como:

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

Esto delega llamadas no contestadas a implementaciones reales.


Sin embargo, con su ejemplo, creo que todavía fallará, ya que la implementación de se getValue()basa en quantityy price, en lugar de getQuantity()y getPrice(), que es lo que se ha burlado.

Otra posibilidad es evitar simulacros por completo:

@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
fuente
21
Creo que esta respuesta es incorrecta. Necesita ESPIRAR una instancia del objeto, no BURLAR la clase.
GaRRaPeTa
2
@GaRRaPeTa Diría que espiar y burlarse son alternativas razonables. Es difícil decir cuál es el mejor para este caso, ya que el OP afirma que este es un ejemplo simplificado.
Jon Newmuis
1
¿No debería ser "Spy" en lugar de "Mock" como cabecera de burla parcial proporcionada por "Spy" de una mejor manera?
Tarun Sapra
2
Stock stock = spy(Stock.class);Esto parece incorrecto, el spymétodo parece aceptar solo objetos, no clases.
Paramvir Singh Karwal
44
+1 por señalar la diferencia entre doReturn(retval).when(spyObj).methodName(args)ywhen(spyObj.methodName(args)).thenReturn(retval)
Captain_Obvious
140

La burla parcial de una clase también es compatible a través de 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());

Consulte los documentos 1.10.19y 2.7.22para obtener una explicación detallada.

Sudarshan
fuente
37

Según los documentos :

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
fuente
2
Gracias por demostrar cómo configurar un simulacro donde se llama a la implementación real para todos los métodos, excepto los pocos que necesito controlar desde la prueba.
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()); }Esto no funciona Cualquiera sea la razón, cuando se ejecuta "cuándo", en realidad ejecuta el método que se supone que debe ser burlado. Código:
Lance Kind
3
El problema es "cuándo". El "cuándo" en realidad ejecutará lo que quiere burlarse parcialmente. Para evitar esto, hay una alternativa: doReturn (). Vea doReturn () en docs.mockito.googlecode.com/hg/1.9.5/org/mockito/…
Lance Kind
18

Lo que quieres está de org.mockito.Mockito.CALLS_REAL_METHODSacuerdo con los documentos:

/**
 * 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>
 */

Por lo tanto, su código debería verse así:

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);
    }
}

La llamada a Stock stock = mock(Stock.class);llamadas org.mockito.Mockito.mock(Class<T>)que se ve así:

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

Los documentos del valor RETURNS_DEFAULTSdicen:

/**
 * 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.)
 */
el tío
fuente
1
Bien visto ... pero ¿puedo preguntar por qué lo usas withSettings()...así? Parece que org.mockito.internal.stubbing.answers.CallsRealMethods()(por ejemplo) podría hacer el trabajo ... y el javadoc para esta clase específicamente dice que es para el uso de los simulacros parciales ...
Mike roedor
3
Además ... ¿no se encontrará con el problema encontrado por otras respuestas aquí: es decir thenReturn, ejecutará realmente el método (lo que podría causar problemas, aunque no en este ejemplo), por lo que doReturnes preferible en tal caso ...?
Mike roedor
4

La burla parcial con el método de espionaje de Mockito podría ser la solución a su problema, como ya se indicó en las respuestas anteriores. Hasta cierto punto, estoy de acuerdo en que, para su caso de uso concreto, puede ser más apropiado burlarse de la búsqueda de DB. Desde mi experiencia, esto no siempre es posible, al menos no sin otras soluciones, que consideraría muy engorroso o al menos frágil. Tenga en cuenta que la burla parcial no funciona con versiones aliadas de Mockito. Tienes uso al menos 1.8.0.

Hubiera escrito un comentario simple para la pregunta original en lugar de publicar esta respuesta, pero StackOverflow no lo permite.

Solo una cosa más: realmente no puedo entender que muchas veces una pregunta que se hace aquí reciba un comentario con "Por qué quieres hacer esto" sin al menos tratar de entender el problema. Especialmente cuando se trata de la necesidad de una burla parcial, realmente hay muchos casos de uso que podría imaginar dónde sería útil. Es por eso que los chicos de Mockito proporcionaron esa funcionalidad. Por supuesto, esta característica no debe ser utilizada en exceso. Pero cuando hablamos de configuraciones de casos de prueba que de otro modo no podrían establecerse de una manera muy complicada, se debe usar el espionaje.

kflGalore
fuente
2
Siento que esta respuesta es parcialmente una opinión. Por favor considere editar.
soundslikeodd
2
Votado para animar al nuevo miembro de la familia. No es necesario tener esta zona en -ve, nada realmente técnicamente incorrecto o idioma / tono incorrecto. Sea amable con los nuevos miembros. Gracias.
Saurabh Patil