3

我有一个 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. IUnityContainer.RegisterInstance正在调用并提供正确的服务。

由于Initialize()负责实际创建具体类型并注册它们,因此在为它提供我自己的ISomeDataService模拟时似乎我很不走运。现在它确实尝试解决具体类型SomeDataService(这与做的事情基本相同new SomeDataService()),所以我可以尝试模拟具体类型SomeDataService并覆盖我想要测试的方法,但是当具体类型有副作用时这会成为一个问题例如 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 一切都很好,但我不知道这是否是正确的方法......我确定我做错了,必须有其他方法.

4

1 回答 1

1

这是个有趣的问题。

查看提供的示例,实际上测试了三件事:

  • Initialize 注册您的服务
  • 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;}
}

或者,您可以使用 Subclass to Test 模式:

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

从上面几个有趣的点:

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

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

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

最后一个是你的争论点。您不想为测试添加 wcf 配置。我同意,但是因为我们在前两个测试中解耦了模块的行为,所以只需要最后一个配置。最后一个测试是一个集成测试,证明你有合适的配置文件;我会用一个集成类别属性标记这个测试,并与其他测试一起运行它,这些测试使用它们的适当配置加载和初始化所有模块。毕竟,关键是要验证这一切是否有效——诀窍是为孤立的组件获得有意义的反馈。

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

希望这可以帮助...

于 2012-12-22T16:58:21.480 回答