对返回具体类的工厂方法进行单元测试

37

我有一个工厂类,正在尝试确定单位测试应该做什么。从这个问题中,我可以验证返回的接口是我期望的特定具体类型。

如果工厂返回的是具体类型(因为目前没有必要使用接口),那么我应该检查什么?目前我正在执行类似以下操作的步骤:

[Test]
public void CreateSomeClassWithDependencies()
{
    // m_factory is instantiated in the SetUp method
    var someClass = m_factory.CreateSomeClassWithDependencies();

    Assert.IsNotNull(someClass);
}
这样做的问题在于,Assert.IsNotNull 似乎有些多余。
此外,我的工厂方法可能会这样设置该特定类的依赖项:
public SomeClass CreateSomeClassWithDependencies()
{
    return new SomeClass(CreateADependency(), CreateAnotherDependency(),
                         CreateAThirdDependency());
}

我希望确保我的工厂方法正确设置了所有这些依赖项。难道没有其他方法可以做到这一点,而不是将那些依赖项设为public/internal属性,然后在单元测试中检查它们吗?(我不太喜欢修改测试对象以适应测试)

编辑:针对Robert Harvey的问题,我正在使用NUnit作为我的单元测试框架(但我认为这不会有太大的区别)


你使用的测试框架是什么? - Robert Harvey
一些测试框架要求您的类是虚拟的,以便测试框架可以继承它们。有些则不需要。这是一个巨大的区别。 - Robert Harvey
5个回答

40

通常情况下,创建公共属性以进行基于状态的测试是没有问题的。是的:它是你创建的代码,用于启用测试场景,但它会伤及你的API吗?其他客户端有可能在以后发现同样的属性有用吗?

测试特定代码和测试驱动设计之间存在微妙的界限。我们不应该引入除了满足测试要求外没有其他潜力的代码,但引入遵循普遍接受的设计原则的新代码是完全可以的。我们让测试来驱动我们的设计-这就是为什么我们称其为TDD :)

向类添加一个或多个属性以给用户更好地检查该类的可能性,在我看来,通常是一个合理的做法,因此我认为你不应该放弃引入这样的属性。

除此之外,我支持nader的答案 :)


2
“让测试驱动我们的设计”是一个好习惯。使用属性来暴露依赖关系既有助于配置依赖项,也使它们更加明显。这两个方面都是好的事情。 - Nader Shirazie
22
有时为了测试而暴露类的内部细节是不恰当的。例如,您不希望意外泄漏一个内部可变对象,因为该类的正确性依赖于它。这可能是有问题的几个原因:(a)违反了封装原则,以及(b)类安全性(其他甚至是恶意代码可能会以意想不到和不希望的方式更改类)。当您不想泄漏抽象内容时,如何测试工厂呢? - Jim Hurne
@Jim Hurne:我同意,而且我从来没有说过其他的话。公开一个只读属性和公开可变引用之间有很大的区别。只有在整体上有意义时,我们才应该这样做(但通常不是这样)。 - Mark Seemann
8
根据你的API使用情况,如果仅仅是为了可能有客户会用到而添加这些方法,我认为在长期运营中会给你带来麻烦。每次这样做,你都需要维护和测试这个方法或者遵循废弃流程 - 以防止某个客户正在使用该方法。这似乎是一种巨大的成本,而收益却非常小;特别是如果你在整个API中经常这样做。我的建议是尽可能保持你的API简洁紧凑 - 要么客户需要它并且应该进行测试,要么就不需要。 - lexicalscope
1
这是微软为了让所有东西都变成“internal sealed”而提出的典型借口。虽然我承认如果你要向数千或数百万消费者发货,必须考虑到这一点,但我仍然坚持认为这个论点并不适用于大多数正在生产的代码。全球生产的大部分代码都有非常有限或众所周知的消费者群体,因此我不同意你可以将微软的论点应用于大多数代码库。 - Mark Seemann
显示剩余3条评论

26
如果工厂返回的是具体类型,并且您保证工厂始终返回具体类型而不是null,那么测试结果就没有太多价值。这样做可以确保,随着时间的推移,这种期望不会被违反,并且不会抛出异常。
此类测试只是确保在未来进行更改时,工厂的行为不会发生变化而不知道。
如果您的语言支持,对于您的依赖项,您可以使用反射。这通常不是最易于维护的方法,并且将测试与实现耦合得非常紧密。您必须决定是否可以接受这种方法。这种方法往往非常脆弱。
但是,您似乎真的想要分离哪些类正在被构造,以及如何调用构造函数。您可能最好使用DI框架来获得这种灵活性。
通过根据需要创建所有类型的new,您不会给自己留下太多的可操作空间(seam是程序中您可以在此处更改行为而无需编辑该位置的地方)。
在您提供的示例中,您可以从工厂派生一个类。然后,覆盖/模拟CreateADependency(),CreateAnotherDependency()和CreateAThirdDependency()。现在,当您调用CreateSomeClassWithDependencies()时,您能够感知是否已创建了正确的依赖项。
注意:“seam”的定义来自Michael Feather的书籍“Working Effectively with Legacy Code”。它包含许多将可测试性添加到未经测试的代码中的技术示例。您可能会发现它非常有用。

2
我喜欢这个答案,如果你需要处理没有测试的遗留或接近遗留(.net 1.1)代码,那么这本书是救星。如果你所在的团队以前没有进行单元测试,并且有很多想要进行单元测试的旧代码,那么这本书非常有用。 - ElvisLives

4
我们所做的是使用工厂创建依赖项,并使用依赖注入框架在运行测试时替换真实工厂的模拟工厂。然后,我们对这些模拟工厂设置适当的期望值。

太好了,但是如果工厂创建的具体类型本身有需要检查的私有属性呢? - Neil

3

您可以始终使用反射来检查内容。没有必要为了单元测试而公开某些内容。我发现需要使用反射的情况相当罕见,这可能是糟糕设计的迹象。

看看您的示例代码,是的,“Assert not null”似乎有些多余,这取决于您设计工厂的方式,有些工厂将从工厂返回null对象,而不是抛出异常。


0

据我理解,您想要测试依赖项是否构建正确并传递到新实例中?

如果我不能使用像Google Guice这样的框架,我可能会像这样做(在这里使用JMock和Hamcrest):

@Test
public void CreateSomeClassWithDependencies()
{
    dependencyFactory = context.mock(DependencyFactory.class);
    classAFactory = context.mock(ClassAFactory.class);

    myDependency0 = context.mock(MyDependency0.class);
    myDependency1 = context.mock(MyDependency1.class);
    myDependency2 = context.mock(MyDependency2.class);
    myClassA = context.mock(ClassA.class);

    context.checking(new Expectations(){{
       oneOf(dependencyFactory).createDependency0(); will(returnValue(myDependency0));
       oneOf(dependencyFactory).createDependency1(); will(returnValue(myDependency1));
       oneOf(dependencyFactory).createDependency2(); will(returnValue(myDependency2));

       oneOf(classAFactory).createClassA(myDependency0, myDependency1, myDependency2);
       will(returnValue(myClassA));
    }});

    builder = new ClassABuilder(dependencyFactory, classAFactory);

    assertThat(builder.make(), equalTo(myClassA));
}

(如果您无法模拟ClassA,则可以使用new将非模拟版本分配给myClassA)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接