【问题标题】:mock or stub for chained call链式调用的模拟或存根
【发布时间】:2011-12-17 03:12:32
【问题描述】:
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。 有什么想法可以让测试用例变得更简单吗?

【问题讨论】:

    标签: java unit-testing mocking mockito


    【解决方案1】:

    我发现 JMockit 更易于使用并且完全切换到它。查看使用它的测试用例:

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

    这里我模拟了 Activity 基类,它来自 Android SKD 并且完全 存根。使用 JMockit,您可以模拟最终的、私有的、抽象的或其他任何事物。

    在您的测试用例中,它看起来像:

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

    【讨论】:

    • 想你的答案,但我正在使用 mockito,无论如何。
    • 请注意,JMockit 有一个专门用于链式调用的注释:@Cascading。此外,在这种情况下,您可能希望使用 NonStrictExpectations 而不是 Expectations,假设不需要验证对模拟方法的调用。
    • 谢谢,我错过了这个注释 ;) 简化了我的单元测试
    【解决方案2】:

    Mockito can handle chained stubs:

    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());
    

    AFAIK,链中的第一个方法返回一个模拟,设置为在第二个链接方法调用时返回您的值。

    Mockito 的作者指出,这应该仅用于遗留代码。更好的做法是将行为推送到您的 CacheContext 中,并提供它自己完成工作所需的任何信息。您从 CacheContext 提取的信息量表明您的班级有feature envy

    【讨论】:

    • 好吧,Szczepan 创建了 Mockito,因为他看到我和其他一些人手工推出了我们自己的模拟,而不是使用 EasyMock,并决定模拟应该更适合 BDD - 所以显然我更喜欢 Mockito!但是他分叉了 EasyMock 来做这件事,因此,是的,EasyMock 很棒。我们站在巨人的肩膀上……
    • 如果其中一个链返回泛型类型,这将不起作用。有其他人遇到过这个问题吗?
    • @Magnilex 其实是in官方出处。如源代码。 “请注意,在大多数情况下,模拟返回模拟是错误的。”另外:警告:常规干净代码很少需要此功能!把它留给遗留代码。模拟一个模拟返回一个模拟,返回一个模拟,(...),返回一些有意义的暗示违反德墨忒耳定律或模拟一个值对象(一个众所周知的反模式)。 github.com/mockito/mockito/blob/master/src/main/java/org/…(大部分在第 1393 行)。
    • @RuifengMa 我从这个文档中猜测它会是@Mock(answer = RETURNS_DEEP_STUBS) - 试一试,让我们知道! static.javadoc.io/org.mockito/mockito-core/2.2.28/org/mockito/…
    【解决方案3】:

    我建议让您的测试用例更简单的是重构您的方法。

    每当我发现自己在测试一种方法时遇到困难,这对我来说都是一种代码味道,我会问为什么很难测试。如果代码很难测试,它可能很难使用和维护。

    在这种情况下,这是因为您的方法链有好几级。也许传入 ctx、cacheEnable 和 cacheExpire 作为参数。

    【讨论】:

    • 是的,但是这些字段是来自运行时的aop上下文,很难简化环境。
    • 在 JMockit 中有一些技术可以做到这一点。您可以将字段模拟到您的对象中,以模拟 AOP 字段注入。或者您可以使用解封装技术使用模拟实例初始化证明字段
    • 你能提供一个例子@doug吗?
    【解决方案4】:

    以防万一您使用的是 Kotlin。 MockK 没有说链接是一种不好的做法,并且很容易让你做this

    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)
    

    【讨论】:

    • 是的,很棒的东西,确实有助于减少样板代码的数量。
    猜你喜欢
    • 2015-11-20
    • 1970-01-01
    • 1970-01-01
    • 2011-07-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-14
    • 2012-12-07
    相关资源
    最近更新 更多