【问题标题】:Design issue: static class only initializes once, breaks unit testing设计问题:静态类只初始化一次,破坏单元测试
【发布时间】:2013-03-21 18:20:04
【问题描述】:

我有一个静态的Configuration 类负责我的整个系统的数据设置。它在其构造函数中从注册表加载某些值,并且它的所有方法都基于这些值。如果它无法从注册表中获取值(如果尚未激活应用程序则可能),它会抛出一个异常,转换为 TypeInitializationException,这对我来说很好。

我使用 NUnit 编写了单元测试,以确保 Configuration 的构造函数正确处理所有情况 - 正常值、空白值、Null 值。每个测试使用相关值初始化注册表,然后调用 Configuration 中的一些方法。 这就是问题所在:NUnit 决定首先运行 Null 测试。它清除注册表,初始化配置,抛出异常 - 一切都很好。但是,因为这是一个静态类,其构造函数刚刚失败 - 它不会为其他测试再次重新构造该类,并且它们都失败了。

即使没有 Null 测试,我也会遇到问题,因为 Configuration 可能(我猜)为所有使用它的类初始化一次。

我的问题是:我应该使用反射来为每个测试重新构建类,还是应该重新设计这个类来检查属性而不是构造函数中的注册表?

【问题讨论】:

  • 您可以将加载代码移动到 Configuration 类的方法中,而不是构造函数中。然后,您可以使用不同的场景对该函数进行单元测试。
  • @rgunawan 同意,这似乎是一个非常简单的修改。只需将构造函数的整个主体移动到一个方法中,并让构造函数简单地调用该方法。然后可以从单元测试中调用相同的方法。看起来很简单。

标签: c# unit-testing nunit static-constructor


【解决方案1】:

我会选择重新设计。 如果出现任何问题,也有一个 TypeInitializationException 可能会使用户/开发人员感到困惑。

【讨论】:

    【解决方案2】:

    我建议调整您的代码以使用单例模式作为第一步。

    【讨论】:

    • ...因为单例不会遭受只初始化一次的缺陷?
    • 如果它是单例,至少出于测试目的,如果出现上述问题,则有可能重建内部类。在我看来,它需要对原始解决方案进行最少的代码更改。
    • 不,最少的代码更改是什么都不做。单例是一种可怕的设计模式,而不是你应该寻求使用的东西。当即使是普及它的书的作者也在回溯并说“我们可能不应该包含它”,这是一种暗示。当然,另一个提示是,作为一种设计模式,它没有提供任何好处,同时结合了其他几种方法的所有缺陷和缺点。简而言之,it sucks
    【解决方案3】:

    我的建议是重新设计你的 Configuration 类。因为你的问题本质上是理论性的,有一点实际的方面(单元测试失败),我会提供一些链接来支持我的想法。

    首先,将Configuration 设为非静态类。谷歌工程师 Miško Hevery 就OO Design for Testability 发表了非常好的演讲,他特别提到全局状态是一个糟糕的设计选择,特别是如果你想测试它。

    您的配置可以通过构造函数接受RegistryProvider 实例(我假设您听说过依赖注入原理)。 RegistryProvider 责任只是从注册表中读取值,这是它应该做的唯一事情。现在,当您测试Configuration 时,您将提供RegistryProvider stub(如果您不知道存根和模拟是什么 - 谷歌它,它们本质上很简单),您将在其中硬编码特定注册表项的值。

    现在,好处:

    • 你有很好的unit tests,因为你不依赖注册表
    • 你没有全局状态(可测试性)
    • 您的测试不依赖于 彼此(每个都有单独的Configuration 实例)
    • 您的测试不依赖于执行它们的环境(您可能没有访问注册表的权限,但您仍然可以测试您的 Configuration 类)

    如果你觉得自己不太擅长依赖注入,我会推荐一个由 Mark Seemann 的天才提供给凡人灵魂的奇妙的艺术和工程作品,名为 Dependency Injection in .NET。我读过的关于类设计的最佳书籍之一,面向 .NET 开发人员。

    为了让我的回答更具体:

    我应该使用反射来为每个测试重新构建类吗?

    不,你不应该在测试中使用反射(只有在没有其他情况的情况下)。反思会让你测试:

    • 脆弱的
    • 很难理解
    • 难以维护

    使用面向对象的实践结合封装来实现实现的隐藏。然后只测试外部行为,不要依赖内部实现细节。这将使您的测试仅依赖于外部行为而不依赖于内部实现,这可能会发生很大变化。

    我应该重新设计这个类来检查属性中的注册表吗 而不是构造函数?

    按照我的回答中所述设计您的课程将使您能够测试您的课程而不访问注册表。这是单元测试的基石——不要依赖外部系统(数据库、文件系统、网络、registry 等...)。如果您想测试您是否可以访问注册表 - 编写单独的集成测试,您将在其中写入注册表并从中读取。

    现在我没有足够的信息告诉你是应该通过Configuration 构造函数中的RegistryProvider 读取注册表,还是按需懒惰地读取注册表,这是一个棘手的问题。但我绝对可以说 - 尽量避免全局状态,尽量减少对测试中实现细节的依赖(这与整个 OO 原则有关)并尝试单独测试对象,即不访问外部系统。然后你可以模仿特殊情况,例如当注册表不可用时,你的类行为是否符合预期?如果您通过Configuration 类的静态成员直接访问注册表,则很难重新创建这样的场景。

    【讨论】:

    • 感谢您的详细回复和链接 - 我刚开始观看讲座,非常棒。
    • 我相信我会按照你的建议做所有事情,尽管我还在考虑依赖注入。如果我在初始化这个类时要求一个注册表存根,那不会增加耦合吗?我所有的消费者现在都知道我正在从注册表中读取这些设置,他们都必须做一些额外的工作......
    • @user884248 好的,您现在可以将 RegistryProvider 抽象为接口 IConfigurationProvider 并将接口传递给构造函数。现在,您可以将这些依赖项与 Composition Root(参见 Mark 的书或文章)联系起来,没有人会知道 Configuration 正在使用 RegistryProvider 类的实例
    【解决方案4】:

    众所周知,静态类/方法很难进行单元测试。
    (另请注意,您当前所做的根本不是 unit 测试;它是 integration 测试(您正在更改测试的注册表值)。)

    恐怕你必须在你的类是可测试的还是静态的之间做出选择。

    您可以做出的妥协是将“逻辑”位(即验证等)移动到不同的非静态类,该类将由主静态类调用。
    然后可以轻松测试该非静态类。

    【讨论】:

      猜你喜欢
      • 2017-09-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-23
      • 2020-09-08
      • 1970-01-01
      • 1970-01-01
      • 2020-01-31
      相关资源
      最近更新 更多