首页 > 解决方案 > 给定多个方法的调用顺序的 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已被调用的对象。或者,如果有一种完全不同的方法可以使测试更清晰。

标签: javamockito

解决方案


在我看来,这是过度工程模拟对象的一个​​很好的例子。

不要试图让你的模拟表现得像“真实的东西”。这不是在编写测​​试时应该使用模拟。

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

如果Service返回给定 ID 的某些内容,或者在没有结果时引发异常,请制作 2 个单独的测试用例!

我们无法预见重构的原因。也许在删除之前会有 n 个调用来检索。所以这实际上是将两个方法的行为捆绑在一起。

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

仅使用存根使其运行

考虑写一个假的 ifService相当简单并且变化不大。请记住,模拟只是一种工具。有时还有其他选择。


考虑到我刚才所说的,这可能会给您发送混杂的信息,但是由于 StackOverflow 已经关闭了一段时间,而我目前正在大量使用 Mockito,所以我花了一些时间来解决您的另一个问题:

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

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)

    }

}

上面的测试可能包含错误(我使用了我目前正在处理的项目中的一些类)。

感谢挑战。


推荐阅读