突变测试是 TDD 的演变

由于测试驱动开发是模仿自然运作方式而建立的,因此突变测试是 DevOps 演变的自然下一步。
129 位读者喜欢这篇文章。
Open ant trail

Opensource.com

在“失败是无责 DevOps 中的一项功能”中,我讨论了通过征求反馈意见来交付质量时,失败所起的核心作用。 这是敏捷 DevOps 团队赖以指导和驱动开发的失败。 测试驱动开发 (TDD) 是任何敏捷 DevOps 价值流交付的必要条件。 以失败为中心的 TDD 方法只有与可衡量的测试相结合才能发挥作用。

TDD 方法是模仿自然运作方式以及自然如何在进化游戏中产生赢家和输家而建立的。

自然选择

Charles Darwin

1859 年,查尔斯·达尔文在他的著作《物种起源》中提出了进化论。 达尔文的论点是,自然变异是由个体生物的自发突变和环境压力共同造成的。 这些压力消除了适应性较差的生物,同时有利于其他更适合的生物。 每个生物都会突变其染色体,并且这些自发突变会传递到下一代(后代)。 然后在新出现的变异中,根据自然选择(由于环境条件的可变性而存在的环境压力)进行测试。

这个简化的图表说明了适应环境条件的过程。

Environmental pressures on fish

图 1. 不同的环境压力导致不同的结果,这些结果受自然选择的支配。 图像是来自 Richard Dawkins 视频的屏幕截图。

此图显示了一群鱼在其自然栖息地中。 栖息地各不相同(海底或河床底部的砾石颜色较深或较浅),每条鱼也各不相同(身体图案和颜色较深或较浅)。

它还显示了两种情况(即,环境压力的两种变化)

  1. 捕食者存在
  2. 捕食者不存在

在第一种情况下,相对于砾石阴影更容易被发现的鱼被捕食者捕获的风险更高。 当砾石颜色较深时,鱼群中颜色较浅的部分会减少。 反之亦然 - 当砾石的颜色较浅时,鱼群中颜色较深的部分会遭受减少的情况。

在第二种情况下,鱼非常放松,可以进行交配。 在没有捕食者且存在交配仪式的情况下,可以预期相反的结果:相对于背景而言,突出的鱼更有可能被选中进行交配,并将其特征传递给后代。

选择标准

在变异中进行选择时,这个过程从来不是任意的、反复无常的、异想天开的或随机的。 决定性因素始终是可衡量的。 决定性因素通常称为测试目标

一个简单的数学示例可以说明这个决策过程。(仅在这种情况下,它不会受自然选择的支配,而是受人工选择的支配。)假设有人要求您构建一个小函数,该函数接受一个正数并计算该数的平方根。 你会怎么做?

敏捷 DevOps 的方式是快速失败。 从谦逊开始,首先承认您真的不知道如何开发该函数。 在这一点上,您只知道如何描述您想要做什么。 用技术术语来说,您已准备好参与制作单元测试

“单元测试”描述了您的具体期望。 它可以简单地表述为“给定数字 16,我希望平方根函数返回数字 4。” 您可能知道 16 的平方根是 4。但是,您不知道一些较大数字(例如 533)的平方根。

至少,您已经制定了您的选择标准,您的测试或目标。

实现失败的测试

.NET Core 平台可以说明该实现。 .NET 通常使用 xUnit.net 作为单元测试框架。(要遵循编码示例,请安装 .NET Core 和 xUnit.net。)

打开命令行并创建一个文件夹,将在其中实现平方根解决方案。 例如,键入

mkdir square_root

然后键入

cd square_root

为单元测试创建一个单独的文件夹

mkdir unit_tests

移动到 unit_tests 文件夹 (cd unit_tests) 并启动 xUnit 框架

dotnet new xunit

现在,向上移动一个文件夹到 square_root 文件夹,并创建 app 文件夹

mkdir app
cd app

创建 C# 代码所需的支架

dotnet new classlib

现在打开您喜欢的编辑器并开始破解!

在您的代码编辑器中,导航到 unit_tests 文件夹并打开 UnitTest1.cs

UnitTest1.cs 中的自动生成代码替换为

using System;
using Xunit;
using app;

namespace unit_tests{

   public class UnitTest1{
       Calculator calculator = new Calculator();

       [Fact]
       public void GivenPositiveNumberCalculateSquareRoot(){
           var expected = 4;
           var actual = calculator.CalculateSquareRoot(16);
           Assert.Equal(expected, actual);
       }
   }
}

此单元测试描述了变量 expected 应为 4 的期望值。下一行描述了 actual 值。 它建议通过向名为 calculator 的组件发送消息来计算 actual 值。 此组件被描述为能够通过接受数值来处理 CalculateSquareRoot 消息。 该组件尚未开发。 但这并不重要,因为这仅仅描述了期望值。

最后,它描述了何时触发发送消息时会发生什么。 在那时,它断言 expected 值是否等于 actual 值。 如果是,则测试通过,并且达到目标。 如果 expected 值不等于 actual value,则测试失败。

接下来,要实现名为 calculator 的组件,请在 app 文件夹中创建一个新文件,并将其命名为 Calculator.cs。 要实现计算数字平方根的函数,请将以下代码添加到这个新文件中

namespace app {
   public class Calculator {
       public double CalculateSquareRoot(double number) {
           double bestGuess = number;
           return bestGuess;
       }
   }
}

在您可以测试此实现之前,您需要指示单元测试如何找到这个新组件 (Calculator)。 导航到 unit_tests 文件夹并打开 unit_tests.csproj 文件。 在 <ItemGroup> 代码块中添加以下行

<ProjectReference Include="../app/app.csproj" />

保存 unit_test.csproj 文件。 现在您已准备好进行第一次测试运行。

转到命令行并 cd 进入 unit_tests 文件夹。 运行以下命令

dotnet test

运行单元测试将产生以下输出

xUnit output after the unit test run fails

图 2. 单元测试运行失败后 xUnit 输出。

如您所见,单元测试失败了。 它期望向 calculator 组件发送数字 16 会导致数字 4 作为输出,但输出 (actual 值) 是数字 16。

恭喜! 您已经创建了您的第一个失败。 您的单元测试提供了强烈、即时的反馈,敦促您修复失败。

修复失败

要修复失败,您必须改进 bestGuess。 现在,bestGuess 只是获取函数接收的数字并返回它。 这还不够好。

但是,您如何找出一种计算平方根值的方法? 我有一个想法 - 如何看看大自然母亲如何解决问题。

通过迭代来模仿大自然母亲

从第一次(也是唯一一次)尝试中猜测正确的值非常困难(几乎是不可能的)。 您必须允许多次尝试猜测,以增加您解决问题的机会。 允许多次尝试的一种方法是迭代

要迭代,请将 bestGuess 值存储在 previousGuess 变量中,转换 bestGuess 值,并比较两个值之间的差。 如果差为 0,则您解决了问题。 否则,继续迭代。

以下是产生任何正数平方根正确值的函数的主体

double bestGuess = number;
double previousGuess;

do {
   previousGuess = bestGuess;
   bestGuess = (previousGuess + (number/previousGuess))/2;
} while((bestGuess - previousGuess) != 0);

return bestGuess;

此循环(迭代)将 bestGuess 值收敛到所需的解决方案。 现在,您精心设计的单元测试通过了!

Unit test successful

图 3. 单元测试成功,0 个测试失败。

迭代解决了问题

就像大自然母亲的方法一样,在本练习中,迭代解决了问题。 增量方法与逐步细化相结合是获得令人满意解决方案的保证方法。 在此游戏中,决定性因素是拥有可衡量的目标和测试。 一旦有了它,您就可以不断迭代,直到达到目标。

现在是关键时刻!

好的,这是一个有趣的实验,但是更有趣的发现来自于玩这个新铸造的解决方案。 到目前为止,您的起始 bestGuess 始终等于该函数接收的数字作为输入参数。 如果您更改初始 bestGuess 会发生什么?

要测试这一点,您可以运行几个场景。 首先,观察逐步细化,因为迭代循环遍历一系列猜测,因为它试图计算 25 的平方根

Code iterating for the square root of 25

图 4. 迭代以计算 25 的平方根。

从 25 作为 bestGuess 开始,该函数需要八次迭代才能计算出 25 的平方根。但是,如果您对 bestGuess 进行了可笑的、极其错误的尝试会发生什么? 如果您从一个毫无线索的第二次猜测开始,即 100 万可能是 25 的平方根? 在如此明显错误的情况下会发生什么? 您的函数是否能够处理如此愚蠢的行为?

看看马的嘴巴。 重新运行该场景,这次从 100 万开始作为 bestGuess

Stepwise refinement

图 5. 通过从 1,000,000 开始作为初始 bestGuess 来计算 25 的平方根时的逐步细化。

哇! 从一个荒谬的大数字开始,迭代次数仅增加了两倍(从八次迭代到 23 次)。 几乎没有像您可能凭直觉预期的那样大幅增加。

故事的寓意

当你意识到迭代不仅能保证解决问题,而且无论你最初的解决方案搜索是从一个好的还是一个非常糟糕的初始猜测开始,都无关紧要时,顿悟时刻就到来了。无论你最初的理解多么错误,迭代的过程,加上一个可衡量的测试/目标,都会让你走上正确的道路并提供解决方案。保证。

图 4 和图 5 显示了一个陡峭而引人注目的燃尽图。从一个非常不正确的起点开始,迭代迅速燃尽到一个绝对正确的解决方案。

简而言之,这种惊人的方法论是敏捷 DevOps 的本质。

回到一些高层次的观察

敏捷 DevOps 实践源于我们认识到我们生活在一个根本上基于不确定性、模糊性、不完整性和适量混乱的世界中。 从科学/哲学的角度来看,这些特征都有充分的记录,并得到海森堡不确定性原理(涵盖不确定性部分)、维特根斯坦的《逻辑哲学论》(模糊性部分)、哥德尔不完备性定理(不完备性方面)和热力学第二定律(持续熵引起的混乱)的支持。

简而言之,无论你多么努力,在尝试解决任何问题时,你永远无法获得完整的信息。 因此,放弃傲慢的姿态,采取更谦逊的方式来解决问题更有利可图。 谦逊的回报是丰厚的——不仅能让你获得期望的解决方案,还能获得结构良好的解决方案的副产品。

结论

大自然不知疲倦地工作——它是一个连续的流动。 大自然没有总体规划; 一切发生都是对先前发生事情的回应。 反馈循环非常紧密,并且明显的进步/倒退是零碎的。 在你看到的任何地方,你都会看到逐步改进,以这样或那样的形式出现。

敏捷 DevOps 是工程模型逐步成熟的一个非常有趣的结果。 DevOps 基于这样一种认识,即你拥有的信息始终是不完整的,因此你最好谨慎行事。 获得一个可衡量的测试(例如,一个假设、一个可衡量的期望),做出一个谦逊的尝试来满足它,很可能会失败,然后收集反馈,修复失败,并继续。 除了同意每一步都必须有一个可衡量的假设/测试之外,没有其他计划。

在本系列文章的下一篇文章中,我将更仔细地研究突变测试如何提供推动价值的急需反馈。

标签
User profile image.
自 1990 年以来,Alex 一直从事软件开发。他目前的热情是如何将柔性带回软件中。他坚信我们的行业已经达到了一个复杂的水平,在这个水平上,这个崇高的目标(即将柔性带回软件中)是完全可以实现的。

9 条评论

有趣的阅读,Alex! 突变的概念解释得很清楚。

感谢分享

感谢您的评论。 如果您继续关注,在下一篇文章中,我将深入研究突变测试,这是确保软件精密工程达到最高质量的最后一道防线。

回复 作者 Armstrong Foundjem

非常有趣的比较。

“当在可变性中进行选择时,这个过程永远不是任意的、反复无常的、异想天开的或随机的。 决定性因素始终是可衡量的。 这个决定性因素通常被称为测试或目标。”

这当然不是完全准确的。 突变过程本身很少给出可行且可重复的替代方案,但从定义上来说是完全随机的,因此是任意的和非常反复无常的。 但是,还有第二次传递,经过大量时间,并在存在适当威胁级别的情况下,会逐渐使任何更合适的形式都获得优势。 那个筛子那么紧吗?

由于通常存在几种不同的威胁,并且不一定同时存在,因此第二次传递也相当随意,并且可能会根据时间安排或是否存在其他物种、环境变化等而给出不同的结果。 我们能看到的“选择的解决方案”有很多种,为了逃避捕食者,有些动物跑得更快,另一些动物爬得更高,还有一些动物擅长躲藏。 如果捕食者 2 赶走了捕食者 1,仅仅是因为它更重且更强大,那么“解决方案”仍然可以挽救那些突变,但如果捕食者 2 具有更好的视力,并且由于它的喂养能力(再次由于随机突变)而胜过了过时的捕食者 1,那么这将使一些猎物的隐藏能力失效。

在开发项目中,显然测试选择了可行的解决方案,而这些解决方案本身可能会因编写代码的人而异。 测试的顺序、深度和覆盖范围也在塑造结果方面发挥作用,然后这还取决于谁在编写它们。

所有这些都受到环境、文化、可用工具和库、可用时间和资源的深刻影响。

总之,自然选择和开发这两个过程的可预测性和数学性质并不是那么明显。 它们是进化性的,因此是机会主义的,时间或环境上的微小差异可能会产生完全不同的结果。 这就是为什么面对同一个问题,两家公司会提出不同的解决方案并展开竞争。

TDD 或“传统”测试都将确保解决方案按预期运行。 在任何一种情况下,测试都可能被遗忘或仓促完成。 解决方案可能会以不同的方式驱动,但我怀疑这种选择会产生任何重大影响。 如果真是这样,那么方法论的选择早就应该淘汰“传统”验证方法,而支持 TDD,毕竟它不是一个新概念。

对这些挑战的有趣看法。 您可能误解了本文的意图。 与自然界不同,软件工程中的迭代不是随机的。 它们遵循一定的算法。 唯一随机的是最初的最佳猜测; 在该初始猜测之后的所有其他内容都是确定性的。

回复 作者 NoahFebak (未验证)

好读,Alex。 一个问题:你是如何推导出“猜测” bestGuess = (previousGuess + (number/previousGuess))/2; 的?
对我来说,这看起来你仍然需要一个相当清楚的关于平方根是如何计算的想法?

如果你想玩工程游戏,你必须具备一些资格。 在软件工程的情况下,您必须通过拥有一个算法库加入游戏。

在这种情况下,我求助于有记录的最古老的算法——巴比伦算法。

但是,希望能够在没有任何先验教育的情况下盲目地解决工程问题是愚蠢的差事。

回复 作者 MartinNotRegistered (未验证)

更正 -- 在上面的回复中,第一句话应该这样说

如果你想玩工程游戏,你必须具备一些资格。

回复 作者 Alex Bunardzic

那么我不明白了。 当然,简单的例子可以用来解释复杂的主题。 但是,一个经过实践验证的迭代算法如何与自然和进化相提并论? 在我理解中,进化不是一个迭代算法,而是迭代该算法(例如,突变)。 随着时间的推移,改变对问题的答案。

回复 作者 Alex Bunardzic

自然和人类发明的工程之间存在巨大差异。 自然拥有世界上所有的时间,而工程受到预算的严重限制(时间和金钱方面的考虑)。

从理论上讲,我们可以设计一个系统,其中提供的正数的平方根的最佳猜测只是某个随机数,然后检查它是否通过了测试,如果没通过,则迭代但进行另一个疯狂的随机猜测。 最终,在宇宙中无限的时间内,随机猜测将是正确的。

你知道那句谚语“万亿只猴子,每只都坐在打字机前,最终会产生莎士比亚的全部作品”。

如果有足够的时间,一切皆有可能。 但在工程领域,我们通过赢得与时间的赛跑来获胜。 先发优势等等。

回复 作者 MartinNotRegistered (未验证)

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 许可。
© . All rights reserved.