Use o Mockito para zombar de alguns métodos, mas não de outros

402

Existe alguma maneira, usando o Mockito, de zombar de alguns métodos de uma classe, mas não de outros?

Por exemplo, nesta Stockclasse (admitidamente inventada) , quero zombar dos valores getPrice()e getQuantity()retornar (como mostrado no trecho de teste abaixo), mas quero getValue()executar a multiplicação conforme codificado na 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
fonte
4
Por que você gostaria de fazer isso? Você deve estar testando a classe (nesse caso, não deve haver zombaria) ou deve fazê-lo enquanto estiver testando uma classe diferente (nesse caso, nenhuma funcionalidade). Por que você faria uma zombaria parcial?
weltraumpirat
3
Ok, este é um pequeno exemplo da coisa real. Na realidade, estou tentando evitar uma chamada ao banco de dados, passando valores inventados, mas quero verificar se os outros métodos funcionam corretamente com esses valores inventados. Existe uma maneira melhor de fazer isso?
Victor Grazi
5
Certamente: mova as chamadas do banco de dados para uma classe separada (a lógica do domínio e o acesso ao banco de dados não devem estar na mesma classe; são duas preocupações diferentes), extraia sua interface, use essa interface para conectar-se a partir da classe de lógica do domínio e zombe apenas do interface durante o teste.
weltraumpirat
11
Concordo plenamente, é difícil explicar a imagem inteira sem fazer upload de grandes quantidades de código aqui, incluindo bibliotecas de terceiros.
Victor Grazi
11
Você provavelmente poderia. Porém, isso não seria "uma maneira melhor de fazer isso": seu código de banco de dados é um detalhe de implementação que você deseja ocultar do restante do seu aplicativo, provavelmente até mover para um pacote diferente. Você não gostaria de recompilar sua lógica de domínio toda vez que alterar uma instrução de sequela, não é?
weltraumpirat

Respostas:

644

Para responder diretamente à sua pergunta, sim, você pode zombar de alguns métodos sem zombar de outros. Isso é chamado de simulação parcial . Consulte a documentação do Mockito sobre zombarias parciais para obter mais informações.

Para o seu exemplo, você pode fazer algo como o seguinte, em seu teste:

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

Nesse caso, cada implementação de método é zombada, a menos que seja especificado thenCallRealMethod()na when(..)cláusula.

Existe também a possibilidade de o contrário com espião, em vez de simulação :

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

Nesse caso, toda a implementação do método é real, exceto se você definiu um comportamento simulado com when(..).

Há uma armadilha importante quando você usa o when(Object)espião, como no exemplo anterior. O método real será chamado (porque stock.getPrice()é avaliado antes when(..)no tempo de execução). Isso pode ser um problema se o seu método contiver lógica que não deve ser chamada. Você pode escrever o exemplo anterior assim:

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

Outra possibilidade pode ser o uso org.mockito.Mockito.CALLS_REAL_METHODS, como:

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

Isso delega chamadas não rotuladas para implementações reais.


No entanto, com o seu exemplo, acredito que ainda falhará, já que a implementação getValue()depende quantitye price, em vez de getQuantity()e getPrice(), é o que você zombou.

Outra possibilidade é evitar zombarias 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
fonte
21
Eu acho que esta resposta está errada. Você precisa espiar uma instância do objeto, não MOCK a classe.
GaRRaPeTa
2
@GaRRaPeTa Eu diria que espionagem e zombaria são alternativas razoáveis. É difícil dizer qual é o melhor para este caso, uma vez que o OP afirma que este é um exemplo simplificado.
Jon Newmuis
11
Não deve ser "Spy" em vez de "Mock", pois cabeçalhos de zombaria parciais são fornecidos pelo "Spy" de uma maneira melhor.
Tarun Sapra
2
Stock stock = spy(Stock.class);Isso parece errado, o spymétodo parece aceitar apenas objetos e não classes.
Paramvir Singh Karwal,
4
+1 para apontar a diferença entre doReturn(retval).when(spyObj).methodName(args)ewhen(spyObj.methodName(args)).thenReturn(retval)
Captain_Obvious 12/11
140

O mocking parcial de uma classe também é suportado via Spy no 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());

Verifique os documentos 1.10.19e 2.7.22para obter explicações detalhadas.

Sudarshan
fonte
37

De acordo com os 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
fonte
2
Obrigado por demonstrar como configurar uma simulação em que a implementação real é chamada para todos os métodos, exceto os poucos que eu preciso controlar a partir do teste.
precisa saber é
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()); }Isso não funciona. Qualquer que seja o motivo, quando "quando" é executado, ele realmente executa o método que deveria ser ridicularizado. Código:
Lance Kind
3
O problema é "quando". O "quando" realmente executará a coisa que você deseja zombar parcialmente. Para evitar isso, existe uma alternativa: doReturn (). Veja o doReturn () em docs.mockito.googlecode.com/hg/1.9.5/org/mockito/…
Lance Kind
18

O que você deseja é de org.mockito.Mockito.CALLS_REAL_METHODSacordo com os 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>
 */

Assim, seu código deve se parecer com:

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

A chamada para Stock stock = mock(Stock.class);chamadas org.mockito.Mockito.mock(Class<T>)assim:

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

Os documentos do valor RETURNS_DEFAULTSinformam:

/**
 * 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.)
 */
o cara
fonte
11
Bem visto ... mas posso perguntar por que você usa withSettings()...assim? Parece que org.mockito.internal.stubbing.answers.CallsRealMethods()(por exemplo) pode fazer o trabalho ... eo javadoc para esta classe especificamente diz que é para uso em simulações parciais ...
mike roedor
3
Além disso ... isso não funcionará no problema encontrado por outras respostas aqui: ou seja thenReturn, executará o método (o que pode causar problemas, embora não neste exemplo) e, portanto, doReturné preferível nesse caso ...?
microfone roedor
4

A zombaria parcial usando o método de espionagem do Mockito pode ser a solução para o seu problema, como já indicado nas respostas acima. Até certo ponto, concordo que, para o seu caso de uso concreto, talvez seja mais apropriado zombar da pesquisa do banco de dados. Pela minha experiência, isso nem sempre é possível - pelo menos não sem outras soluções alternativas - que consideraria muito complicadas ou pelo menos frágeis. Observe que essa zombaria parcial não funciona com versões aliadas do Mockito. Você usou pelo menos 1.8.0.

Eu teria acabado de escrever um comentário simples para a pergunta original, em vez de postar esta resposta, mas o StackOverflow não permite isso.

Só mais uma coisa: eu realmente não consigo entender que muitas vezes uma pergunta está sendo feita aqui, com o comentário "Por que você quer fazer isso" sem ao menos tentar entender o problema. Especialmente quando se trata de zombar parcialmente, há muitos casos de uso que eu poderia imaginar onde seria útil. É por isso que os caras da Mockito forneceram essa funcionalidade. Obviamente, esse recurso não deve ser usado em excesso. Mas quando falamos sobre configurações de casos de teste que de outra forma não poderiam ser estabelecidas de uma maneira muito complicada, a espionagem deve ser usada.

kflGalore
fonte
2
Eu sinto que esta resposta é parcialmente uma opinião. Por favor, considere a edição.
soundslikeodd
2
Voto a favor para animar o novo membro da família. Não há necessidade de obter essa zona in-ve, nada realmente tecnicamente errado ou idioma / tom incorretos. Seja gentil com os novos membros.
Saurabh Patil