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
已被调用的对象。或者,如果有一种完全不同的方法可以使测试更清晰。
解决方案
在我看来,这是过度工程模拟对象的一个很好的例子。
不要试图让你的模拟表现得像“真实的东西”。这不是在编写测试时应该使用模拟。
测试不是关于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)
}
}
上面的测试可能包含错误(我使用了我目前正在处理的项目中的一些类)。
感谢挑战。
推荐阅读
- elasticsearch - ElasticSearch - 完全匹配等于操作的单词
- postgresql - 教师 SQL 的课数
- javascript - 吊装说明
- python - Python - 在数据框中查找值并返回随机对应值
- java - 提醒通知未在 android O 及更高版本中显示
- javascript - 将 ImageData 转换为 JS 中的 blob?
- emacs - Emacs - 如何让弹丸在当前窗口中打开文件?
- python - Django -AttributeError:'str'对象没有属性'objects'
- android - Android Studio:不允许操作(绑定失败)
- python - 如何在 Python 中处理包含合并 (colspan = 2) 列的 html 表(最好使用 Beautifulsoup)?