我的应用程序使用第三方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.
}
}
这看起来像是许多样板代码,只是为了启用单元测试。它闻起来很难闻,但我可能错了。有更好的方法吗?
有一种类似但更简单的方法:向工厂添加一个受保护的方法来创建Foo:
protected Foo create(String theirArg){
return new Foo(theirArg);
}
然后在工厂的测试中,创建FactoryImpl的Test Double并覆盖创建方法:
private class FooFactoryImplTestDouble extends FooFactoryImpl{
...
@Override
protected Foo create(String theirArg){
//create and return your spy here
}
}
创建一个新类:
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。
事实证明,我无论如何都必须使用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 } )
退一步想想FooFactoryImpl
的合同是什么。它必须创建一个功能齐全的Foo
,不管这意味着什么。因此,如果Foo
的契约是做X、Y和Z,那么FooFactoryImpl
的契约是创建做X、Y和Z的对象。
这是一种SUT由多个类组成的测试的情况。我不在乎您是否将其称为单元测试、集成测试、子系统测试、协作测试或其他名称。关键是FooFactoryImpl
的唯一有意义的测试是测试Foo
的测试。与其单独为Foo
编写一个测试类,不如编写一个联合测试这两个类的测试类。
因此,如果Foo
的约定是执行X、Y和Z,那么您的测试用例将使用FooFactoryImpl
执行以下操作。
- 调用
create
并测试创建的对象是否执行X - 调用
create
并测试创建的对象是否为Y - 调用
create
并测试创建的对象是否执行Z
我相信这是解决这个问题的唯一明智方法。困难的部分是为测试类想出一个令人信服的名字。