簡體   English   中英

給定多個方法的調用順序,在Mockito中進行條件存根

[英]conditional stubbing in Mockito given invocation order of multiple methods

有沒有一種干凈的方法可以根據其他方法的調用來更改模擬方法的行為?

測試中的代碼示例, 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())

我想知道例如是否可以實現一個可以檢測是否已調用deleteByIdAnswer對象。 或者,如果有完全不同的方法可以使測試更清潔。

在我眼中,這是過度設計模擬對象的一個​​很好的例子。

不要試圖使模擬行為像“真實的事物”一樣 不是 編寫測試時應該使用的模擬方法

測試不是關於Service本身,而是關於使用它的某個類。

如果Service針對給定的ID返回某項內容,或者在沒有結果的情況下引發異常,請進行2個單獨的測試用例!

我們無法預見到重構的原因。.也許在刪除之前將有n個調用以進行檢索。.因此,這實際上是將兩種方法的行為聯系在一起。

是的,有人可以添加另外十二種方法,這些方法都會影響deleteById的結果。 你會跟蹤嗎?

僅使用存根使其運行

如果Service很簡單並且變化不大,請考慮編寫偽造品。 記住模擬只是一種工具。 有時還有其他選擇。


考慮到我剛才說的話,這可能會向您發送混合消息,但是由於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)

    }

}

上面的測試可能包含錯誤(我使用了當前正在處理的項目中的某些類)。

感謝您的挑戰。

您可以使用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());

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM