我的应用程序使用第三方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对此工厂进行单元测试 - 确保已正确配置创建的对象。但当然,我遇到this problem;也就是说,因为我的工厂叫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.
}
}
这似乎是很多样板代码只是为了启用单元测试。对我而言smells bad,但我可能错了。还有更好的方法吗?
答案 0 :(得分: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
}
}
答案 1 :(得分: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。
答案 2 :(得分: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 } )
答案 3 :(得分:0)
退一步思考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。我相信这是解决这个问题的唯一合理方法。困难的部分是为测试课提供一个令人信服的名字。