给定多个方法的调用顺序,在Mockito中进行条件存根

时间:2018-10-04 16:00:17

标签: java mockito

是否有一种干净的方法可以基于其他方法的调用来更改模拟方法的行为?

正在测试的代码示例service将由Mockito在测试中嘲笑:

public Bar foo(String id) {
  Bar b = service.retrieveById(id);
  boolean flag = service.deleteById(id);
  b = service.retrieveById(id);  //this should throw an Exception
  return b;
}

在这里,我们希望service.retrieveById返回一个对象,除非调用了service.delete

在这种简单情况下,束缚行为可能有效,但是它不会考虑调用另一种方法deleteById(想象中的重构)。

when(service.retrieveById(any())).
  .thenReturn(new Bar())
  .thenThrow(new RuntimeException())

例如,我想知道是否有可能实现一个Answer对象,该对象可以检测是否已调用deleteById。或者,如果有一种完全不同的方法可以使测试更清洁。

2 个答案:

答案 0 :(得分:2)

在我眼中,这是过度模仿工程对象的一个​​很好的例子。

请勿尝试使模拟行为表现为“真实的事物” 。 那不是不是 编写测试时应该使用什么模拟

测试不是关于Service本身,而是关于某个使用它的类。

如果Service返回给定ID的值,或者在没有结果的情况下引发异常,则进行2个单独的测试用例!

  

我们无法预见重构的原因。.也许在删除之前将有n个调用进行检索。.因此,这实际上是将两种方法的行为联系在一起。

是的,有人可以添加另外十二种方法,这些方法都会影响deleteById的结果。你会跟踪吗?

仅使用存根即可使其运行。

如果Service很简单并且变化不大,请考虑编写伪造的东西。记住模拟只是一种工具。有时还有其他选择。


考虑到我刚才所说的内容,这可能会向您发送混合消息,但是由于StackOverflow停顿了一段时间,而且我目前正与Mockito进行大量合作,因此我花了一些时间来回答另一个问题:

  

例如,我想知道是否有可能实现一个Answer对象,该对象可以检测是否已调用deleteById。

import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.util.function.Supplier;

import static java.util.Objects.requireNonNull;


/**
 * An Answer that resolves differently depending on a specified condition.
 *
 * <p>This implementation is NOT thread safe!</p>
 *
 * @param <T> The result type
 */
public class ConditionalAnswer <T> implements Answer<T> {


    /**
     * Create a new ConditionalAnswer from the specified result suppliers.
     *
     * <p>On instantiation, condition is false</p>
     *
     * @param whenConditionIsFalse  The result to supply when the underlying 
              condition is false
     * @param whenConditionIsTrue The result to supply when the underlying 
              condition is true
     * @param <T> The type of the result to supply
     * @return A new ConditionalAnswer
     */
    public static <T> ConditionalAnswer<T> create (
            final Supplier<T> whenConditionIsFalse,
            final Supplier<T> whenConditionIsTrue) {

        return new ConditionalAnswer<>(
                requireNonNull(whenConditionIsFalse, "whenConditionIsFalse"),
                requireNonNull(whenConditionIsTrue, "whenConditionIsTrue")
        );

    }


    /**
     * Create a Supplier that on execution throws the specified Throwable.
     *
     * <p>If the Throwable turns out to be an unchecked exception it will be
     *  thrown directly, if not it will be wrapped in a RuntimeException</p>
     *
     * @param throwable The throwable
     * @param <T> The type that the Supplier officially provides
     * @return A throwing Supplier
     */
    public static <T> Supplier<T> doThrow (final Throwable throwable) {

        requireNonNull(throwable, "throwable");

        return () -> {

            if (RuntimeException.class.isAssignableFrom(throwable.getClass())) {
                throw (RuntimeException) throwable;
            }

            throw new RuntimeException(throwable);

        };

    }


    boolean conditionMet;
    final Supplier<T> whenConditionIsFalse;
    final Supplier<T> whenConditionIsTrue;



    // Use static factory method instead!
    ConditionalAnswer (
            final Supplier<T> whenConditionIsFalse, 
            final Supplier<T> whenConditionIsTrue) {

        this.whenConditionIsFalse = whenConditionIsFalse;
        this.whenConditionIsTrue = whenConditionIsTrue;

    }



    /**
     * Set condition to true.
     *
     * @throws IllegalStateException If condition has been toggled already
     */
    public void toggle () throws IllegalStateException {

        if (conditionMet) {
            throw new IllegalStateException("Condition can only be toggled once!");
        }

        conditionMet = true;

    }


    /**
     * Wrap the specified answer so that before it executes, this 
     * ConditionalAnswer is toggled.
     *
     * @param answer The ans
     * @return The wrapped Answer
     */
    public Answer<?> toggle (final Answer<?> answer) {

        return invocation -> {
            toggle();
            return answer.answer(invocation);
        };


    }


    @Override
    public T answer (final InvocationOnMock invocation) throws Throwable {

        return conditionMet ? whenConditionIsTrue.get() : whenConditionIsFalse.get();

    }


    /**
     * Test whether the underlying condition is met
     * @return The state of the underlying condition
     */
    public boolean isConditionMet () {
        return conditionMet;
    }


}

我写了一些测试来使其工作。这就是将其应用于Service示例的样子:

@Test
void conditionalTest (
        @Mock final Service serviceMock, @Mock final Bar barMock) {

        final var id = "someId"

        // Create shared, stateful answer
        // First argument: Untill condition changes, return barMock
        // Second: After condition has changed, throw Exception
        final var conditional = ConditionalAnswer.create(
                () -> barMock,
                ConditionalAnswer.doThrow(new NoSuchElementException(someId)));

        // Whenever retrieveById is invoked, the call will be delegated to 
        // conditional answer
        when(service.retrieveById(any())).thenAnswer(conditional);


        // Now we can define, what makes the condition change.
        // In this example it is service#delete but it could be any other
        // method on any other class


        // Option 1: Easy but ugly
        when(service.deleteById(any())).thenAnswer(invocation -> {
           conditional.toggle();
           return Boolean.TRUE;
        });


        // Option 2: Answer proxy
        when(service.deleteById(any()))
                .thenAnswer(conditional.toggle(invocation -> Boolean.TRUE));


        // Now you can retrieve by id as many times as you like
        assertSame(barMock, serviceMock.retrieveById(someId));
        assertSame(barMock, serviceMock.retrieveById(someId));
        assertSame(barMock, serviceMock.retrieveById(someId));
        assertSame(barMock, serviceMock.retrieveById(someId));
        assertSame(barMock, serviceMock.retrieveById(someId));

        // Until
        assertTrue(serviceMock.deleteById(someId));

        // NoSuchElementException
        serviceMock.retrieveById(someId)

    }

}

上面的测试可能包含错误(我使用了当前正在研究的项目中的某些类)。

感谢挑战。

答案 1 :(得分:0)

您可以使用Mockito.verify()来检查是否调用了deleteById

Mockito.verify(service).deleteById(any());

您还可以使用Mockito.InOrder进行有序验证(我尚未测试以下代码):

InOrder inOrder = Mockito.inOrder(service);
inOrder.verify(service).retrieveById(any());
inOrder.verify(service).deleteById(any());
inOrder.verify(service).retrieveById(any());