模拟抽象的其余部分 class 但调用其中的真实方法?

Mock the rest of an abstract class but call the real methods in it?

有一些接口叫做 Foo

interface Foo {
    void add(Object key, Object value);
    Object get(Object key);
    void someOtherMethodUnessentialToTesting();
}

测试中有一个名为 MockFoo 的实现,它以 "default" 的方式实现了大多数方法(什么都不做,返回 null,基本上只实现它们以便编译)。但是,它实现了几个提供实际功能的方法,它们是插入和读取 Map 的方法。 (如果最后一点不是必需的,我只会使用 Mockito 模拟,甚至不会问。)

// The current "bad mock"
class MockFoo implements Foo {

    Map<Object, Object> map = new ...

    @Override
    void add(Object key, Object value) {
        map.put(key, value);
    }

    @Override
    Object get(Object key) {
        map.get(key);
    }

    @Override
    void someOtherMethodUnessentialToTesting()
    {}   
}

问题是因为这不是 Mockito 模拟,每次界面更改时都必须更新测试。是的,人们应该更好地检查以修复所有实现他们更改了界面,但我认为 确实 不应该首先在测试中实现。

我对如何解决这个问题感到困惑。我的直觉是将其抽象化并仅实现那些方法,然后以某种方式模拟它,以便它在需要时调用那些 "real" 方法。我读到 Mockito 有一个用于存根的 thenDoRealMethod() 但这仅用于返回值,因此它不适用于 void 方法。

// My abstract class I was trying to stub somehow
abstract class FooForTest implements Foo {

    Map<Object, Object> map = new ...

    @Override
    void add(Object key, Object value) {
        map.put(key, value);
    }

    @Override
    Object get(Object key) {
        map.get(key);
    } 
}

我意识到这可能是一个设计问题,在实际代码中添加 AbstractFoo 可能是最好的(因为添加和获取不会真正改变)但我更好奇是否有只需修改测试代码即可一劳永逸地解决此问题。

使用 this SO answer 中的技术,您可以使用 CALLS_REAL_METHODS 作为默认答案,或者,如您所建议的,您可以使用默认答案 (RETURNS_DEFAULTS) 并单独使用存根某些方法来调用你的假货。因为你想让 Mockito 的行为表现出来,所以我推荐后者。

void 方法有一个等价于 thenCallRealMethod 的方法:doCallRealMethod。您需要从 do 开始,因为 when(T value) 没有任何价值可以接受 [并随后忽略]。

abstract class FooForTest implements Foo {

    public static Foo create() {
        FooForTest mockFoo = mock(FooForTest.class);
        mockFoo.map = new HashMap<>();
        when(mockFoo.get(any())).thenCallRealMethod();
        doCallRealMethod().when(mockFoo).add(any(), any());
        return mockFoo;
    }        

    Map<Object, Object> map = new ...

    @Override
    void add(Object key, Object value) {
        map.put(key, value);
    }

    @Override
    Object get(Object key) {
        map.get(key);
    }
}

没有复杂的反射,我想不出有什么办法可以自动让Mockito只为未实现的方法调用真正的方法。但是,如果您要为此编写一个 Answer,则可以通过将其传递到对 mock 的调用来将其用于所有方法。您还需要在 create 方法中显式初始化您的字段,因为模拟不是真实对象并且不会被正确初始化。

另一种选择是使用空方法存根,并使用 spy() 代替;这将解决一些初始化问题。


此技术称为 部分模拟,请注意它可以充当 code smell——具体来说,被测 class 违反了单一职责原则。出于所有这些原因,在任何情况下,在向广泛实现的接口添加方法时都应该非常小心,并考虑 AbstractFoo(或完全实现的 FakeFoo)作为以后的明智升级。