突变测试示例:将失败视为实验

开发一个自动化猫门的逻辑,使其在白天打开并在夜间锁定,并使用 .NET xUnit.net 测试框架进行操作。
128 位读者喜欢这个。
Out of the trash and into the classroom

Opensource.com

在本系列的第一篇文章中,我演示了如何使用计划内的失败来确保代码中的预期结果。在第二篇文章中,我将继续开发我的示例项目——一个自动化猫门,它在白天打开并在夜间锁定。

提醒一下,您可以按照此处的说明使用 .NET xUnit.net 测试框架进行操作。

白天时间呢?

回想一下,测试驱动开发 (TDD) 以大量的单元测试为中心。

第一篇文章实现了满足 Given7pmReturnNighttime 单元测试期望的逻辑。但是您还没有完成。现在您需要描述当前时间大于早上 7 点时会发生什么。这是新的单元测试,名为 Given7amReturnDaylight

       [Fact]
       public void Given7amReturnDaylight()
       {
           var expected = "Daylight";
           var actual = dayOrNightUtility.GetDayOrNight();
           Assert.Equal(expected, actual);
       }

新的单元测试现在失败了(尽早失败是非常可取的!)

Starting test execution, please wait...
[Xunit.net 00:00:01.23] unittest.UnitTest1.Given7amReturnDaylight [FAIL]
Failed unittest.UnitTest1.Given7amReturnDaylight
[...]

它期望接收字符串值“Daylight”,但实际上接收到字符串值“Nighttime”。

分析失败的测试用例

仔细检查后,似乎代码把自己困住了。事实证明,GetDayOrNight 方法的实现是不可测试的!

看看我们所面临的核心挑战

  1. GetDayOrNight 依赖于隐藏的输入。

    dayOrNight 的值取决于隐藏的输入(它从内置系统时钟获取一天中的时间值)。
  2. GetDayOrNight 包含非确定性行为。

    从系统时钟获得的一天中的时间值是非确定性的。它取决于您运行代码的时间点,我们必须认为这是不可预测的。
  3. GetDayOrNight API 的质量低下。

    此 API 与具体数据源(系统 DateTime)紧密耦合。
  4. GetDayOrNight 违反了单一职责原则。

    您实现了一个同时消耗和处理信息的方法。一个方法应该负责执行单一职责是一个好的实践。
  5. GetDayOrNight 有多个更改理由。

    可以想象内部时间源可能会更改的场景。此外,也很容易想象处理逻辑会发生变化。这些不同的更改原因必须彼此隔离。
  6. 当试图理解 GetDayOrNight 的行为时,其 API 签名是不够的。

    非常希望能够通过简单地查看 API 的签名来了解从 API 期望获得什么类型的行为。
  7. GetDayOrNight 依赖于全局共享的可变状态。

    应该不惜一切代价避免共享的可变状态!
  8. 即使在阅读源代码之后,也无法预测 GetDayOrNight 方法的行为。

    这是一个可怕的命题。从阅读源代码中应该始终非常清楚,一旦系统运行,可以预测什么样的行为。

失败背后的原则

每当您面临工程问题时,建议使用经过时间考验的分而治之策略。在这种情况下,遵循关注点分离原则是可行的方法。

关注点分离 (SoC) 是一种设计原则,用于将计算机程序分成不同的部分,以便每个部分处理一个单独的关注点。关注点是一组影响计算机程序代码的信息。关注点可以像代码正在优化的硬件的细节一样通用,也可以像要实例化的类的名称一样具体。一个很好地体现了 SoC 的程序称为模块化程序。

来源

GetDayOrNight 方法应该只关注于决定日期和时间值是否意味着白天或夜间。它不应该关注于查找该值的来源。该关注点应留给调用客户端。

您必须将其留给调用客户端来负责获取当前时间。这种方法符合另一个有价值的工程原则——控制反转。Martin Fowler 在此处详细探讨了这个概念。

框架的一个重要特征是,用户定义的用于定制框架的方法通常将从框架本身内部调用,而不是从用户的应用程序代码调用。框架通常在协调和排序应用程序活动中扮演主程序的角色。这种控制反转使框架具有充当可扩展骨架的能力。用户提供的方法为特定应用程序定制框架中定义的通用算法。

-- Ralph Johnson 和 Brian Foote

重构测试用例

所以代码需要重构。摆脱对内部时钟(DateTime 系统实用程序)的依赖

 DateTime time = new DateTime();

删除上面的行(应该是文件中的第 7 行)。通过向 GetDayOrNight 方法添加输入参数 DateTime time 来进一步重构您的代码。

这是重构后的类 DayOrNightUtility.cs

using System;

namespace app {
   public class DayOrNightUtility {
       public string GetDayOrNight(DateTime time) {
           string dayOrNight = "Nighttime";
           if(time.Hour >= 7 && time.Hour < 19) {
               dayOrNight = "Daylight";
           }
           return dayOrNight;
       }
   }
}

重构代码需要更改单元测试。您需要为 nightHourdayHour 准备值,并将这些值传递到 GetDayOrNight 方法中。以下是重构后的单元测试

using System;
using Xunit;
using app;

namespace unittest
{
   public class UnitTest1
   {
       DayOrNightUtility dayOrNightUtility = new DayOrNightUtility();
       DateTime nightHour = new DateTime(2019, 08, 03, 19, 00, 00);
       DateTime dayHour = new DateTime(2019, 08, 03, 07, 00, 00);

       [Fact]
       public void Given7pmReturnNighttime()
       {
           var expected = "Nighttime";
           var actual = dayOrNightUtility.GetDayOrNight(nightHour);
           Assert.Equal(expected, actual);
       }

       [Fact]
       public void Given7amReturnDaylight()
       {
           var expected = "Daylight";
           var actual = dayOrNightUtility.GetDayOrNight(dayHour);
           Assert.Equal(expected, actual);
       }

   }
}

经验教训

在继续进行这个简单的场景之前,回顾一下并复习本练习中的经验教训。

通过实现不可测试的代码很容易不经意地创建一个陷阱。从表面上看,这样的代码可能看起来运行正常。但是,遵循测试驱动开发 (TDD) 实践——首先描述期望,然后再规定实现——揭示了代码中的严重问题。

这表明 TDD 是确保代码不会变得过于混乱的理想方法。TDD 指出了问题区域,例如缺少单一职责和存在隐藏输入。此外,TDD 还有助于删除非确定性代码,并用行为具有确定性的完全可测试代码替换它。

最后,TDD 帮助交付了易于阅读的代码和易于理解的逻辑。

在本系列的下一篇文章中,我将演示如何使用本次练习中创建的逻辑来实现功能代码,以及进一步的测试如何使其变得更好。

标签
User profile image.
Alex 自 1990 年以来一直从事软件开发。他目前的激情是如何将“软”带回软件。他坚信,我们的行业已经达到了相当成熟的水平,可以完全实现这个崇高的目标(即将“软”带回软件)。

评论已关闭。

© . All rights reserved.