测试第三方级别的工厂



我的应用程序使用第三方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

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

相关内容

  • 没有找到相关文章

最新更新