【问题标题】:Unit Testing Composition Roots?单元测试组合根源?
【发布时间】:2012-12-20 16:46:28
【问题描述】:

我有一个 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 一切都很好,但我不知道这是否是正确的方法......我确定我做错了,必须有其他方式。

【问题讨论】:

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


    【解决方案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;}
    }
    

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

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

    以上几个有趣的点:

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

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

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

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

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

    希望这会有所帮助...

    【讨论】:

      猜你喜欢
      • 2023-03-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-04-12
      • 1970-01-01
      • 1970-01-01
      • 2016-01-06
      相关资源
      最近更新 更多