3

我的应用程序使用第三方 jar(无法访问源等)。我有一个工厂可以Foo从设置中正确创建一个对象(调用它),即

public FooFactoryImpl implements FooFactory {
    private final Settings settings;
    private final OtherDependency other;

    @Inject
    public FooFactoryImpl(Settings settings, OtherDependency other) {
        this.settings = settings;
        this.other = other;
    }

    public Foo create(String theirArg) {
        Foo newFoo = new Foo(theirArg); // there is no no-arg constructor

        // This isn't exactly the way I do it but this is shorter and close enough
        newFoo.setParamOne(settings.get("ParamOne")); 
        newFoo.setParamTwo(settings.get("ParamTwo"));
        // etc.
    }
}

我想使用 Mockito 对这个工厂进行单元测试——确保创建的对象配置正确。但是,当然,我遇到了这个问题;也就是说,因为我的工厂调用new,我不能注入间谍。

一种可能的解决方案是引入如下内容:

public FooFactoryDumb implements FooFactory {
    public Foo create(String theirArg) {
        return new Foo(theirArg);
    }
}

然后是这样的:

public FooFactoryImpl implements FooFactory {
    @Inject @Dumb private FooFactory inner;

    // snip, see above

    public create(String theirArg) {
        Foo newFoo = inner.create(theirArg);
        // etc.
    }
}

这似乎是很多样板代码,只是为了启用单元测试。我闻起来很臭,但我可能错了。有没有更好的办法?

4

4 回答 4

2

有一种类似但更简单的方法:向您的工厂添加一个受保护的方法来创建一个 Foo:

protected Foo create(String theirArg){
    return new Foo(theirArg);
}

然后在您的 Factory 测试中,创建FactoryImpl的Test Double并覆盖 create 方法:

private class FooFactoryImplTestDouble extends FooFactoryImpl{
    ...
    @Override
    protected Foo create(String theirArg){

        //create and return your spy here
    }
}
于 2013-08-23T14:57:37.687 回答
0

好吧,事实证明我无论如何都必须使用PowerMock,因为第三方的方法是最终的。由于我已经在使用 PowerMock,我意识到我可以这样做:

@Before
public void setUp() throws Exception {
    Foo toReturn = PowerMockito.mock(Foo.class);
    PowerMockito.whenNew(Foo.class).withAnyArguments().thenReturn(toReturn);
}

然后我根本不必碰我原来的课程。

注意:如果你这样做,你必须为 PowerMock 准备两个类,即

@PrepareForTest( { Foo.class, FooFactoryImpl.class } )
于 2013-08-23T18:03:18.963 回答
0

创建一个新类:

public class FooFactory3rd {
    public Foo create3rdParty(String theirArg) {
        return new Foo(theirArg);
    }
}

然后将您的课程更改为:

public FooFactoryImpl implements FooFactory {
    private final Settings settings;
    private final OtherDependency other;
    private final FooFactory3rd fooFactory3rd;

    @Inject
    public FooFactoryImpl(Settings settings, OtherDependency other, FooFactory3rd fooFactory3rd) {
        this.settings = settings;
        this.other = other;
        this.fooFactory3rd = fooFactory3rd;
    }

    public Foo create(String theirArg) {
        Foo newFoo = fooFactory3rd.create3rdParty(theirArg);

        // This isn't exactly the way I do it but this is shorter and close enough
        newFoo.setParamOne(settings.get("ParamOne")); 
        newFoo.setParamTwo(settings.get("ParamTwo"));
        // etc.
    }
}

在您的测试代码中:

Foo fooMock = mock(Foo.class);
FooFactory3rd fooFactory3rdMock = mock(FooFactory3rd.class);
when(fooFactory3rdMock.create3rdParty(any(String.class)).thenReturn(fooMock);

FooFactoryImpl fooFactoryImpl = new FooFactoryImpl(settings, other, fooFactory3rdMock);
fooFactoryImpl.create("any string");

这样,您可以注入您的 fooMock。当你打电话时fooFactoryImpl.create("any string"),你的嘲笑 Foo 在封面下被调用。

或者如果你想更干净,甚至不需要 FooFactory3rd 的构造函数 arg。只需声明

    private final FooFactory3rd fooFactory3rd = new FooFactory3rd();

在您的测试中,使用反射将其更改为模拟的 FooFactory3rd。

于 2013-08-23T15:14:08.370 回答
0

退后一步,想想合同FooFactoryImpl是什么。就是它必须创建一个功能齐全的Foo,无论这意味着什么。因此,如果 a 的约定Foo是它执行 X、Y 和 Z,那么 a 的约定FooFactoryImpl是它创建执行 X、Y 和 Z 的对象。

这是 SUT 包含多个类的测试的一种情况。我不在乎您是否称其为单元测试、集成测试、子系统测试、协作测试或其他名称。关键是,唯一有意义的测试FooFactoryImpl是同时测试的测试Foo。与其Foo单独编写一个测试类,不如编写一个共同测试两个类的测试类。

因此,如果 的合同Foo是做 X、Y 和 Z,那么您的测试用例将用FooFactoryImpl.

  • 调用create并测试创建的对象是否执行 X。
  • 调用create并测试创建的对象是否执行 Y。
  • 调用create并测试创建的对象是否执行 Z。

我相信这是解决这个问题的唯一明智的方法。困难的部分是为测试类取一个令人信服的名称。

于 2013-08-24T03:45:15.543 回答