单元测试组成根?

时间:2012-12-20 16:46:29

标签: c# unit-testing dependency-injection prism

我有一个PRISM应用程序,它由几个模块(IModule)组成,其中引导程序将每个模块传递给DI容器,以便每个模块能够注入/解析服务。这意味着每个模块都有自己的“组合根”,其中注入/解析了类型,我想知道关于单元测试它们的最佳实践。

例如,假设我有一个资源模块,负责创建和注册从各种数据源获取数据的服务。假设我按如下方式实现了IModule.Initialize方法:

void Initialize()
{
ISomeDataService someDataService = _container.Resolve<SomeDataService>();
someDataService.Connect();
_container.RegisterInstance<ISomeDataService>(someDataService);
}

Resources模块创建SomeDataService的实例,打开连接并注册它,以便其他模块可以使用它。注意:这实际上并不是我这样做的,这只是为了快速说明。

现在从单元测试的角度来看,如何测试Initialize方法?我想在这里测试两件事:

    正在调用
  1. ISomeDataService.Connect()方法。
  2. 正在调用
  3. IUnityContainer.RegisterInstance并提供正确的服务。
  4. 由于Initialize()负责实际创建具体类型并注册它们,所以在提供我自己的ISomeDataService模拟时,我似乎不走运。现在它确实尝试解析具体类型SomeDataService(这与执行new SomeDataService()基本相同),所以我可以尝试模拟具体类型SomeDataService并覆盖我想要的方法测试但是当具体类型具有诸如ChannelFactory之类的副作用时会出现问题,ChannelFactory会在实例化时立即尝试解析有效​​的WCF绑定并在失败时抛出异常。我可以通过提供有效的绑定来避免这种失败,但我不认为单元测试应该依赖于这些事情。

    有什么建议吗?我有一个想法如下:

    void Initialize()
    {
    if (_container.IsRegistered<ISomeDataService>())
       {
       someDataService = _container.Resolve<ISomeDataService>();
       }
    else
       {
       someDataService = _container.Resolve<SomeDataService>(); // or new SomeDataService()
       }
    
    _container.RegisterInstance<ISomeDataService>(someDataService);
    someDataService.Connect();
    }
    

    通过这种方式我可以模拟ISomeDataService而不是具体类型SomeDataService,但一切都很好,但我不知道这是否是正确的方法......我确定我做错了,必须有其他方式。

1 个答案:

答案 0 :(得分:1)

这是一个有趣的问题。

查看提供的示例,实际上有三件事正在测试中:

  • 初始化注册您的服务
  • ISomeDataService调用Connect
  • SomeDataService已正确实例化。

通常情况下,我会将Connect推迟到其他后续点,因为这类似于在构造函数中执行工作,并且它表明该模块正在执行多个操作。如果你要删除Connect方法,这将是微不足道的测试。但是,你的需求可能会有所不同,所以我离题了......

这三件事中的每一件都应该是单独的测试。诀窍是找到适当的“接缝”来将实例化与注册分离,并用模拟替换服务,这样我们就可以验证Connect方法。

以上是对上述内容的一个小改动:

public void Initialize()
{
    ISomeDataService service = DataService;
    service.Connect();
    _container.RegisterInstance<ISomeDataService>(service);
}

internal ISomeDataService DataService
{
  get { return _service ?? _service = _container.Resolve<SomeDataService>(); }
  set { _service = value;}
}

或者,您可以使用子类测试模式:

protected internal virtual ISomeDataService GetDataService()
{
  return _container.Resolve<SomeDataService>();
}

以上几点有趣:

  1. 您可以通过为测试对象分配模拟服务来测试注册,调用Initialize然后尝试手动从容器中解析服务。断言已解析的服务与您的模拟实例相同。

  2. 您可以通过分配模拟测试Connect,调用Initialize然后验证是否已调用Connect。

  3. 您可以通过使用适当的依赖项填充容器来测试服务是否可以实例化,并从DataService属性或基本GetDataService()中检索实例(如果您使用的是要测试的子类)。

  4. 这是最后一个对你而言的争论点。您不希望为测试添加wcf配置。我同意,但是因为我们在前两个测试中解耦了模块的行为,所以只需要最后一个配置。最后一个测试是一个集成测试,证明你有适当的配置文件;我将使用Integration类别属性标记此测试,并将其与其他测试一起运行,该测试使用适当的配置加载和初始化所有模块。毕竟,关键在于验证一切正常 - 诀窍是为隔离组件获得有意义的反馈。

    最后一点,您的问题中显示的代码建议您通过填充模拟来测试主题。这与我在这里提出的非常类似,但主要区别在于语义:mock是主题责任的一部分,它不是通过容器注入的依赖。通过这种方式编写它很清楚什么是模块的一部分以及什么是必需的依赖。

    希望这会有所帮助......