链式调用的模拟或存根化

107
protected int parseExpire(CacheContext ctx) throws AttributeDefineException {
    Method targetMethod = ctx.getTargetMethod();
    CacheEnable cacheEnable = targetMethod.getAnnotation(CacheEnable.class);
    ExpireExpr cacheExpire = targetMethod.getAnnotation(ExpireExpr.class);
    // check for duplicate setting
    if (cacheEnable.expire() != CacheAttribute.DO_NOT_EXPIRE && cacheExpire != null) {
        throw new AttributeDefineException("expire are defined both in @CacheEnable and @ExpireExpr");
    }
    // expire time defined in @CacheEnable or @ExpireExpr
    return cacheEnable.expire() != CacheAttribute.DO_NOT_EXPIRE ? cacheEnable.expire() : parseExpireExpr(cacheExpire, ctx.getArgument());
}

那是测试的方法,

Method targetMethod = ctx.getTargetMethod();
CacheEnable cacheEnable = targetMethod.getAnnotation(CacheEnable.class);

我需要模拟三个CacheContext、Method和CacheEnable。有没有什么办法可以使测试用例更简单?

5个回答

212

Mockito 可以处理链式存根

Foo mock = mock(Foo.class, RETURNS_DEEP_STUBS);

// note that we're stubbing a chain of methods here: getBar().getName()
when(mock.getBar().getName()).thenReturn("deep");

// note that we're chaining method calls: getBar().getName()
assertEquals("deep", mock.getBar().getName());

据我所知,链中的第一个方法返回一个模拟对象,该对象设置为在第二个链接方法调用时返回您的值。
Mockito的作者指出这应该仅用于旧代码。否则,更好的做法是将行为推入CacheContext,并提供执行任务所需的任何信息。从CacheContext中提取的信息量表明您的类存在feature envy

但这总是特性嫉妒吗?我有一个DTO树,可以在内存中组织我的大量数据。我有其他类从这个树中读取数据并对其进行操作。为了测试它们,我需要模拟DTO - 顺便说一句,我可以创建实际的DTO,但这同样麻烦 - 深度存根在这种情况下确实帮助了我。我错过了什么吗?我能让我的代码更好吗? - Arash
1
如果其中一个链返回了通用类型,这将无法正常工作。还有其他人遇到过这个问题吗? - Vivek Kothari
2
特征嫉妒的定义已经移至此处:https://github.com/troessner/reek/blob/master/docs/Feature-Envy.md - Sámal Rasmussen
4
这段内容实际上是来自官方源代码,意思是在大多数情况下,返回一个模拟的模拟通常是错误的,这表明可能违反了Demeter法则或者有对值对象进行模拟的情况(这是一个众所周知的反模式)。警告:这种特性很少需要用于常规的干净代码!请将其留给遗留代码。这些信息可以在此处查看:https://github.com/mockito/mockito/blob/master/src/main/java/org/mockito/Mockito.java(其中大部分信息位于第1393行)。 - Lunivore
1
@RuifengMa 我猜从这份文档中可以得出 @Mock(answer = RETURNS_DEEP_STUBS) 的答案 - 不妨试试,并告诉我们结果!https://static.javadoc.io/org.mockito/mockito-core/2.2.28/org/mockito/Mock.html - Lunivore
显示剩余15条评论

11

如果你正在使用Kotlin,Mockk并不认为链式调用是一种不好的实践,并且可以轻松地让你实现这个

val car = mockk<Car>()

every { car.door(DoorType.FRONT_LEFT).windowState() } returns WindowState.UP

car.door(DoorType.FRONT_LEFT) // returns chained mock for Door
car.door(DoorType.FRONT_LEFT).windowState() // returns WindowState.UP

verify { car.door(DoorType.FRONT_LEFT).windowState() }

confirmVerified(car)

是的,这个工具很棒,真的有助于减少样板代码的数量。 - AbstractVoid

5

对于扩展Lunivore的回答的内容,对于任何注入模拟bean的人,请使用:

@Mock(answer=RETURNS_DEEP_STUBS)
private Foo mockedFoo;

3

我建议简化你的测试用例的方法是重构你的方法。

每当我发现自己无法很好地测试一个方法时,我会注意到这可能存在代码问题,并问自己为什么难以测试。如果代码难以测试,那么使用和维护它也可能很困难。

在这种情况下,原因是你有一个深度达数层的方法链。也许可以通过作为参数传递ctx、cacheEnable和cacheExpire来解决。


是的,但这些字段来自运行时的AOP上下文,很难简化环境。 - jilen
在JMockit中有技巧可以做到这一点。您可以模拟AOP字段注入,将字段模拟到您的对象中。或者您可以使用deencapsulation技术,使用模拟实例初始化私有字段。 - Konstantin Pribluda
能否请你举个例子,@doug? - ScottyBlades

2
我发现 JMockit 更易于使用并完全切换到它。请参考以下使用 JMockit 的测试用例:

https://github.com/ko5tik/andject/blob/master/src/test/java/de/pribluda/android/andject/ViewInjectionTest.java

在这里,我模拟了来自 Android SDK 的 Activity 基类,并将其完全存根化。使用 JMockit,您可以模拟 final、private、abstract 或其他任何东西。
在您的测试用例中,它看起来像这样:
public void testFoo(@Mocked final Method targetMethod, 
                    @Mocked  final CacheContext context,
                    @Mocked final  CacheExpire ce) {
    new Expectations() {
       {
           // specify expected sequence of infocations here

           context.getTargetMethod(); returns(method);
       }
    };

    // call your method
    assertSomething(objectUndertest.cacheExpire(context))

1
请注意,JMockit有一个专门用于链式调用的注释:@Cascading。此外,在这种情况下,您可能希望使用NonStrictExpectations而不是Expectations,假设对模拟方法的调用不需要进行验证。 - Rogério
谢谢,我错过了这个注释 ;) 将简化我的单元测试。 - Konstantin Pribluda

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接