简体   繁体   中英

conditional stubbing in Mockito given invocation order of multiple methods

Is there a clean way to change a mock's method behavior based on other method's invocation?

Example of code under test, service will be mocked by Mockito in the test:

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

Here, we would like service.retrieveById to return an object, unless service.delete has been called.

Chaining behaviours could work in this simple case, but it doesn'd consider the invocation of the other method deleteById (imagine refactoring).

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

I am wondering for example if it's possible to implement an Answer object which can detect whether deleteById has been invoked. Or if there is a totally different approach which would make the test cleaner.

In my eyes, this is a good example of over-engeneering mock objects.

Don't try to make your mocks behave like "the real thing" . That is not what mocking should be used for when writing tests .

The test is not about Service itself, it's about some class that makes use of it.

If Service either returns something for a given Id, or raises an exception when there is no result, make 2 individual test cases!

we can't foresee the reason of the refactoring.. maybe there will be n call to retrieve before the delete.. So this is really about tying the two methods behavior together.

Yes, and someone could add another twelve methods that all influence the outcome of deleteById . Will you be keeping track?

Use stubbing only to make it run .

Consider writing a fake if Service is rather simple and doesn't change much. Remember mocking is just one tool. Sometimes there are alternatives.


Considering what I've just said, this might send you mixed messages but since StackOverflow was down for a while and I'm currently working heavily with Mockito myself, I spent some time with your other question:

I am wondering for example if it's possible to implement an Answer object which can detect whether deleteById has been invoked.

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


}

I wrote some tests to make it work. This is how it would look applied to the Service example:

@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)

    }

}

The test above might contain errors (I used some classes from the project that I am currently working on).

Thanks for the challenge.

You can use Mockito.verify() to check whether deleteById was called or not:

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

You can also use Mockito.InOrder for orderly verification (I have not tested the below code):

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM