【问题标题】:Unit testing an abstract factory that takes parameters单元测试一个接受参数的抽象工厂
【发布时间】:2012-02-02 23:21:01
【问题描述】:

给定一个抽象工厂实现:

public class FooFactory : IFooFactory {
   public IFoo Create(object param1, object param2) {
      return new Foo(param1, param2);
   }
}

将为这个类编写哪些单元测试?如何验证 param1 和 param2 是否已转发到 Foo 的创建?我必须制作 Foo 的这些公共属性吗?那不会破坏封装吗?还是我应该把它留给集成测试?

【问题讨论】:

  • 单元测试应该取决于被测试组件的功能需求和期望。如果没有上下文的其余部分,我看不到对该类进行单元测试的任何价值。
  • @ivowiblo 我认为您将单元测试与 BDD 样式测试混淆了。如果您不为此单元编写测试,那么您怎么知道它是否有效?
  • 进行单元测试,但您将测试什么?该单位的期望是什么?我说我看不到在不知道其他任何东西的情况下对该类进行单元测试的价值。仅仅因为进行单元测试而进行单元测试根本没有意义。测试(单元、行为、完整的功能,无论你想要什么)都应该始终由期望和要求驱动。如果不是,那只是势利(如果这个词存在的话)。
  • @ivowiblo 测试确保将正确的参数传递给创建 Foo。
  • 除了提高覆盖率之外,还有什么价值?

标签: c# dependency-injection abstract-factory


【解决方案1】:

以下是我如何为这样的工厂编写几个单元测试之一(使用xUnit.net):

[Fact]
public void CreateReturnsInstanceWithCorrectParam1()
{
    var sut = new FooFactory();
    var expected = new object();
    var actual = sut.Create(expected, new object());
    var concrete = Assert.IsAssignableFrom<Foo>(actual);
    Assert.Equal(expected, concrete.Object1);
}

它会破坏封装吗?是的,不是的……一点点。封装不仅仅关乎数据隐藏——更重要的是,它关乎protecting the invariants of objects

假设 Foo 公开了这个公共 API:

public class Foo : IFoo
{
    public Foo(object param1, object param2);

    public void MethodDefinedByInterface();

    public object Object1 { get; }
}

虽然Object1 属性稍微违反了Law of Demeter,但它不会与类的不变量混淆,因为它是只读的。

此外,Object1 属性是具体 Foo 类的一部分,而不是 IFoo 接口:

public interface IFoo
{
    void MethodDefinedByInterface();
}

一旦您意识到在松散耦合的 API concrete members are implementation details 中,这种具体只读属性对封装的影响非常小。这样想:

公共 Foo 构造函数也是具体 Foo 类的 API 的一部分,因此只需检查公共 API,我们就知道 param1param2 是该类的一部分.从某种意义上说,这已经“破坏了封装”,因此将每个参数作为具体类的只读属性提供并没有太大变化。

这些属性提供的好处是我们现在可以对工厂返回的 Foo 类的结构形状进行单元测试。

这比必须重复一组行为单元测试要容易得多,我们必须假设这些单元测试已经涵盖了具体的 Foo 类。这几乎就像一个合乎逻辑的证明:

  1. 通过对具体 Foo 类的测试,我们知道它正确使用/与其构造函数参数交互。
  2. 从这些测试中,我们还知道构造函数参数是作为只读属性公开的。
  3. 通过对 FooFactory 的测试,我们知道它返回具体 Foo 类的实例。
  4. 此外,我们从 Create 方法的测试中得知,它的参数已正确传递给 Foo 构造函数。
  5. Q.E.D.

【讨论】:

  • 是的,有道理。我喜欢区分接口的公共 API 和具体类的想法。
【解决方案2】:

嗯,大概是这些参数使返回的IFoo 具有真实性。测试返回的实例是否属实。

【讨论】:

  • 这是否意味着您需要了解相关 IFoo 的内部结构?
  • 没有。你传入这些参数是有原因的,以获得一些预期的行为。那是什么行为?测试它。 FooFactory.Create 的规范应该说类似“创建一个实现 IFoo 的对象,这样 P(param1, param2) 其中 P 是一个依赖于 param1param2 的谓词。”测试 P(param1, param2) 对于返回的对象是否为真!
  • 所以...假设 Foo 不平凡并且已经被 N 个测试覆盖,您基本上需要编写 N 个新的、几乎相同的测试来测试返回的 Foo 实例的行为是与 Create 方法的输入一致。听起来你会间接耦合测试。可维护性问题将随之而来。
【解决方案3】:

您可以使 FooFactory 成为一个泛型类,期望 Foo 作为参数,并将其指定为模拟。 调用 factory.Create(...) 然后创建一个模拟实例,然后您可以确认模拟接收到您期望的参数。

【讨论】:

  • 亲爱的哥德尔没有。这是在测试实现细节。
  • @Jason: 更糟糕...它提出了为该工厂创建工厂以简化生成对象的问题,但如果 JontyMC 要求...
【解决方案4】:

工厂通常不进行单元测试,至少根据我的经验 - 对它们进行单元测试没有太大价值。因为它是该接口-实现映射的实现,因此它指的是具体实现。因此,模拟Foo 是无法检查它是否接收到传入的那些参数。

另一方面,DI 容器通常作为工厂工作,因此如果您使用的是工厂,则不需要工厂。

【讨论】:

  • 当依赖依赖于仅在运行时知道的参数或生命周期比消费者的生命周期短时,需要抽象工厂:stackoverflow.com/questions/1993397/…
  • 大多数 DI 框架会在运行时传递参数。他们还可以进行生命周期管理。
【解决方案5】:

这取决于 Foo 对 2 个输入对象的作用。检查 Foo 对它们所做的可以轻松查询的操作。例如,如果在对象上调用方法(包括 getter 或 setter),则提供可以验证是否调用了预期事物的模拟或存根。如果 IFoo 上有可以测试的东西,您可以通过模拟对象传递已知值以在创建后进行测试。对象本身不需要公开可用以验证您是否通过了它们。

我还可以验证是否返回了预期的类型 (Foo),具体取决于测试的其他行为是否取决于 IFoo 实现之间的差异。

如果可能导致任何错误情况,我也会尝试强制执行这些情况,如果您为其中一个或两个对象传入 null 会发生什么情况等。当然,Foo 将单独测试,因此您可以在 FooFactory 测试期间假设 Foo 做了它应该做的。

【讨论】:

  • 你会测试返回类型吗?这到底是在买你什么?您已经设置了所有这些抽象,现在您将编写一个测试,该测试会在您更改实现细节时立即中断。哎呀。
  • @Jason 如果我要为多个工厂编写测试,每个工厂都创建一个 IFoo 实现,我会检查每个工厂是否返回我期望的具体类型。我质疑是否有必要测试一个工厂,它做的很少,但在所要求的范围内......
  • 没有。这不是工厂消费者可以依赖的行为(返回类型为IFoo),因此不应对其进行测试。
  • @Jason “真正的”消费者绝对不应该依赖于返回类型,你是对的。但是,测试可以取决于您需要做什么来验证正确的行为。我会编辑说“可能”,因为确实并不总是需要寻找的东西。
  • 测试也不应该依赖它!他们和其他消费者一样真实。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-19
相关资源
最近更新 更多