突变测试是 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 输出。

如您所见,单元测试失败了。它期望将数字 16 发送到 calculator 组件将导致数字 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 的平方根时的逐步改进。

哇!从一个荒谬的巨大数字开始,迭代次数仅增加了两倍(从 8 次迭代到 23 次)。远没有您可能直观预期的那么剧烈增加。

故事的寓意

当您意识到不仅迭代保证可以解决问题,而且无论您对解决方案的搜索是从一个好的还是糟糕的初始猜测开始都无关紧要时,顿悟时刻就到来了。无论您的初始理解多么错误,迭代过程,加上可衡量的测试/目标,都会将您引向正确的方向并交付解决方案。有保证的。

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

简而言之,这种令人惊叹的方法论是敏捷 DevOps 的精髓。

回到一些高层次的观察

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

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

结论

自然界不知疲倦地运作——这是一个持续的流动。自然界没有总体规划;一切的发生都是对先前发生的事情的回应。反馈循环非常紧密,明显的进步/倒退是零星的。在自然界的任何地方,您都会看到一种或另一种形式的逐步改进。

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

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

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

9 条评论

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

感谢分享

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

回复 作者:Armstrong Foundjem

非常有趣的比较。

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

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

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

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

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

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

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

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

回复 作者:NoahFebak (未验证)

写得很好,Alex。一个问题:您是如何推导出“猜测” bestGuess = (previousGuess + (number/previousGuess))/2; 的?
对我来说,这看起来您仍然需要对平方根的计算方法有一个非常清晰的概念?

如果您想玩工程游戏,您必须具备一些资格。就软件工程而言,您必须通过拥有大量算法来加入游戏。

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

但是,希望能够在没有任何先前教育的情况下盲目地解决工程问题是徒劳的。

回复 作者:MartinNotRegistered (未验证)

更正——在上面的回复中,第一句话应为

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

回复 作者:Alex Bunardzic

那么我不明白。当然,简单的例子有助于解释复杂的主题。但是,经过实践检验的迭代算法如何与自然和进化相提并论呢?在我的理解中,进化不是迭代算法,而是迭代算法本身(例如突变)。随着时间的推移改变问题的答案。

回复 作者:Alex Bunardzic

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

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

您知道那句谚语“一百万只猴子,每只猴子都坐在打字机旁,最终会创作出莎士比亚的完整作品”。

如果有足够的时间,一切皆有可能。但在工程世界中,我们通过赢得与时间的赛跑来获胜。先发优势以及所有这些爵士乐。

回复 作者:MartinNotRegistered (未验证)

© . All rights reserved.