标签 变异测试 下的文章

基于 .NET 的 xUnit.net 测试框架,开发一款自动猫门的逻辑,让门在白天开放,夜间锁定。

在本系列的第一篇文章中,我演示了如何使用设计的故障来确保代码中的预期结果。在第二篇文章中,我将继续开发示例项目:一款自动猫门,该门在白天开放,夜间锁定。

在此提醒一下,你可以按照此处的说明使用 .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 的值取决于隐藏输入(它从内置系统时钟中获取一天的时间值)。

  1. GetDayOrNight 包含非确定性行为。

从系统时钟中获取到的时间值是不确定的。(因为)该时间取决于你运行代码的时间点,而这一点我们认为这是不可预测的。

  1. GetDayOrNight API 的质量差。

该 API 与具体的数据源(系统 DateTime)紧密耦合。

  1. GetDayOrNight 违反了单一责任原则。

该方法实现同时使用和处理信息。优良作法是一种方法应负责执行一项职责。

  1. GetDayOrNight 有多个更改原因。

可以想象内部时间源可能会更改的情况。同样,很容易想象处理逻辑也将改变。这些变化的不同原因必须相互隔离。

  1. 当(我们)尝试了解 GetDayOrNight 行为时,会发现它的 API 签名不足。

最理想的做法就是通过简单的查看 API 的签名,就能了解 API 预期的行为类型。

  1. GetDayOrNight 取决于全局共享可变状态。

要不惜一切代价避免共享的可变状态!

  1. 即使在阅读源代码之后,也无法预测 GetDayOrNight 方法的行为。

这是一个严重的问题。通过阅读源代码,应该始终非常清晰,系统一旦开始运行,便可以预测出其行为。

失败背后的原则

每当你遇到工程问题时,建议使用久经考验的 分而治之 divide and conquer 策略。在这种情况下,遵循 关注点分离 separation of concerns 的原则是一种可行的方法。

关注点分离(SoC)是一种用于将计算机程序分为不同模块的设计原理,以便每个模块都可以解决一个关注点。关注点是影响计算机程序代码的一组信息。关注点可以和要优化代码的硬件的细节一样概括,也可以和要实例化的类的名称一样具体。完美体现 SoC 的程序称为模块化程序。

出处

GetDayOrNight 方法应仅与确定日期和时间值表示白天还是夜晚有关。它不应该与寻找该值的来源有关。该问题应留给调用客户端。

必须将这个问题留给调用客户端,以获取当前时间。这种方法符合另一个有价值的工程原理—— 控制反转 inversion of control 。Martin Fowler 在这里详细探讨了这一概念。

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

Ralph Johnson and Brian Foote

重构测试用例

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

 DateTime time = new DateTime();

删除上述代码(在你的文件中应该是第 7 行)。通过将输入参数 DateTime 时间添加到 GetDayOrNight 方法,进一步重构代码。

这是重构的类 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 帮助交付易于阅读、逻辑易于遵循的代码。

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


via: https://opensource.com/article/19/9/mutation-testing-example-failure-experimentation

作者:Alex Bunardzic 选题:lujun9972 译者:Morisun029 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

使用事先设计好的故障以确保你的代码达到预期的结果,并遵循 .NET xUnit.net 测试框架来进行测试。

在变异测试是 TDD 的演变 一文中,我谈到了迭代的力量。在可度量的测试中,迭代能够保证找到问题的解决方案。在那篇文章中,我们讨论了迭代法帮助确定实现计算给定数字平方根的代码。

我还演示了最有效的方法是找到可衡量的目标或测试,然后以最佳猜测值开始迭代。正如所预期的,第一次测试通常会失败。因此,必须根据可衡量的目标或测试对失败的代码进行完善。根据运行结果,对测试值进行验证或进一步加以完善。

在此模型中,学习获得解决方案的唯一方法是反复失败。这听起来有悖常理,但它确实有效。

按照这种分析,本文探讨了在构建包含某些依赖项的解决方案时使用 DevOps 的最佳方法。第一步是编写一个预期结果失败的用例。

依赖性问题是你不能依赖它们

正如 迈克尔·尼加德 Michael Nygard 在《没有终结状态的架构》中机智的表示的那样,依赖问题是一个很大的话题,最好留到另一篇文章中讨论。在这里,你将会看到依赖项给项目带来的一些潜在问题,以及如何利用测试驱动开发(TDD)来避免这些陷阱。

首先,找到现实生活中的一个挑战,然后看看如何使用 TDD 解决它。

谁把猫放出来?

 title=

在敏捷开发环境中,通过定义期望结果开始构建解决方案会很有帮助。通常,在 用户故事 user story 中描述期望结果:

我想使用我的家庭自动化系统(HAS)来控制猫何时可以出门,因为我想保证它在夜间的安全。

现在你已经有了一个用户故事,你需要通过提供一些功能要求(即指定验收标准)来对其进行详细说明。 从伪代码中描述的最简单的场景开始:

场景 1:在夜间关闭猫门

  • 用时钟监测到了晚上的时间
  • 时钟通知 HAS 系统
  • HAS 关闭支持物联网(IoT)的猫门

分解系统

开始构建之前,你需要将正在构建的系统(HAS)进行分解(分解为依赖项)。你必须要做的第一件事是识别任何依赖项(如果幸运的话,你的系统没有依赖项,这将会更容易,但是,这样的系统可以说不是非常有用)。

从上面的简单场景中,你可以看到所需的业务成果(自动控制猫门)取决于对夜间情况监测。这种依赖性取决于时钟。但是时钟是无法区分白天和夜晚的。需要你来提供这种逻辑。

正在构建的系统中的另一个依赖项是能够自动访问猫门并启用或关闭它。该依赖项很可能取决于具有 IoT 功能的猫门提供的 API。

依赖管理面临快速失败

为了满足依赖项,我们将构建确定当前时间是白天还是晚上的逻辑。本着 TDD 的精神,我们将从一个小小的失败开始。

有关如何设置此练习所需的开发环境和脚手架的详细说明,请参阅我的上一篇文章。我们将重用相同的 NET 环境和 xUnit.net 框架。

接下来,创建一个名为 HAS(“家庭自动化系统”)的新项目,创建一个名为 UnitTest1.cs 的文件。在该文件中,编写第一个失败的单元测试。在此单元测试中,描述你的期望结果。例如,当系统运行时,如果时间是晚上 7 点,负责确定是白天还是夜晚的组件将返回值 Nighttime

这是描述期望值的单元测试:

using System;
using Xunit;

namespace unittest
{
   public class UnitTest1
   {
       DayOrNightUtility dayOrNightUtility = new DayOrNightUtility();

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

至此,你可能已经熟悉了单元测试的结构。快速复习一下:在此示例中,通过给单元测试一个描述性名称Given7pmReturnNighttime 来描述期望结果。然后,在单元测试的主体中,创建一个名为 expected 的变量,并为该变量指定期望值(在该示例中,值为 Nighttime)。然后,为实际值指定一个 actual(在组件或服务处理一天中的时间之后可用)。

最后,通过断言期望值和实际值是否相等来检查是否满足期望结果:Assert.Equal(expected, actual)

你还可以在上面的列表中看到名为 dayOrNightUtility 的组件或服务。该模块能够接收消息GetDayOrNight,并且返回 string 类型的值。

同样,本着 TDD 的精神,描述的组件或服务还尚未构建(仅为了后面说明在此进行描述)。构建这些是由所描述的期望结果来驱动的。

app 文件夹中创建一个新文件,并将其命名为 DayOrNightUtility.cs。将以下 C# 代码添加到该文件中并保存:

using System;

namespace app {
   public class DayOrNightUtility {
       public string GetDayOrNight() {
           string dayOrNight = "Undetermined";
           return dayOrNight;
       }
   }
}

现在转到命令行,将目录更改为 unittests 文件夹,然后运行:

[Xunit.net 00:00:02.33] unittest.UnitTest1.Given7pmReturnNighttime [FAIL]
Failed unittest.UnitTest1.Given7pmReturnNighttime
[...]

恭喜,你已经完成了第一个失败的单元测试。单元测试的期望结果是 DayOrNightUtility 方法返回字符串 Nighttime,但相反,它返回是 Undetermined

修复失败的单元测试

修复失败的测试的一种快速而粗略的方法是将值 Undetermined 替换为值 Nighttime 并保存更改:

using System;

namespace app {
   public class DayOrNightUtility {
       public string GetDayOrNight() {
           string dayOrNight = "Nighttime";
           return dayOrNight;
       }
   }
}

现在运行,成功了。

Starting test execution, please wait...

Total tests: 1. Passed: 1. Failed: 0. Skipped: 0.
Test Run Successful.
Test execution time: 2.6470 Seconds

但是,对值进行硬编码基本上是在作弊,最好为 DayOrNightUtility 方法赋予一些智能。修改 GetDayOrNight 方法以包括一些时间计算逻辑:

public string GetDayOrNight() {
    string dayOrNight = "Daylight";
    DateTime time = new DateTime();
    if(time.Hour < 7) {
        dayOrNight = "Nighttime";
    }
    return dayOrNight;
}

该方法现在从系统获取当前时间,并与 Hour 比较,查看其是否小于上午 7 点。如果小于,则处理逻辑将 dayOrNight 字符串值从 Daylight 转换为 Nighttime。现在,单元测试通过。

测试驱动解决方案的开始

现在,我们已经开始了基本的单元测试,并为我们的时间依赖项提供了可行的解决方案。后面还有更多的测试案例需要执行。

在下一篇文章中,我将演示如何对白天时间进行测试以及如何在整个过程中利用故障。


via: https://opensource.com/article/19/9/mutation-testing-example-tdd

作者:Alex Bunardzic 选题:lujun9972 译者:Morisun029 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出