在“失败是无责 DevOps 的一项特性”一文中,我讨论了失败通过征求反馈意见在交付质量方面发挥的核心作用。这是敏捷 DevOps 团队赖以指导他们并驱动开发的失败。测试驱动开发 (TDD) 是任何敏捷 DevOps 价值流交付的必要条件。只有当失败为中心的 TDD 方法与可衡量的测试相结合时,它才能发挥作用。
TDD 方法论是以自然运作方式以及自然如何在进化博弈中产生赢家和输家为模型的。
自然选择

1859 年,查尔斯·达尔文在他的著作《物种起源》中提出了进化论。达尔文的论点是,自然变异是由个体生物的自发突变和环境压力共同造成的。这些压力淘汰了适应性较差的生物,同时偏爱其他更适应的生物。每个生物都会使其染色体发生突变,而这些自发突变会传递给下一代(后代)。新出现的变异然后在自然选择(由于环境条件的可变性而存在的环境压力)下进行测试。
这张简化的图表说明了适应环境条件的过程。

图 1. 不同的环境压力导致由自然选择支配的不同结果。图片:Richard Dawkins 视频的屏幕截图。
此图显示了一群鱼在其自然栖息地中。栖息地各不相同(海底或河床底部的砾石颜色较深或较浅),每条鱼也各不相同(身体图案和颜色较深或较浅)。
它还显示了两种情况(即环境压力的两种变化)
- 捕食者存在
- 捕食者不存在
在第一种情况下,与砾石阴影相比更容易被发现的鱼类,被捕食者捕食的风险更高。当砾石颜色较深时,鱼类种群中颜色较浅的部分会减少。反之亦然——当砾石颜色较浅时,鱼类种群中颜色较深的部分会遭受减少的情况。
在第二种情况下,鱼类足够放松以进行交配。在没有捕食者且存在交配仪式的情况下,可以预期相反的结果:与背景形成鲜明对比的鱼类更有可能被挑选出来进行交配,并将其特征传递给后代。
选择标准
在变异中进行选择时,该过程绝不是任意的、反复无常的、异想天开的或随机的。决定性因素始终是可衡量的。这个决定性因素通常被称为测试或目标。
一个简单的数学例子可以说明这个决策过程。(只有在这种情况下,它才不会受自然选择支配,而是受人工选择支配。)假设有人请您构建一个小函数,该函数将接受一个正数并计算该数的平方根。您将如何去做呢?
敏捷 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
运行单元测试将产生以下输出

图 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 值收敛到所需的解决方案。现在,您精心制作的单元测试通过了!

图 3. 单元测试成功,0 个测试失败。
迭代解决了问题
就像大自然母亲的方法一样,在本练习中,迭代解决了问题。增量方法与逐步改进相结合是获得令人满意的解决方案的保证方法。这场博弈的决定性因素是有一个可衡量的目标和测试。一旦您有了这些,您就可以不断迭代,直到您达到目标。
现在是点睛之笔!
好的,这是一个有趣的实验,但更有趣的发现来自于使用这个新创建的解决方案。到目前为止,您的起始 bestGuess 始终等于函数接收的作为输入参数的数字。如果您更改初始 bestGuess 会发生什么?
要测试这一点,您可以运行几个场景。首先,观察当迭代循环遍历一系列猜测以尝试计算 25 的平方根时,逐步改进的过程

图 4. 迭代计算 25 的平方根。
从 25 作为 bestGuess 开始,该函数需要八次迭代才能计算出 25 的平方根。但是,如果您对 bestGuess 进行了可笑的、荒谬的错误尝试会发生什么?如果您从一个毫无头绪的第二个猜测开始,即 100 万可能是 25 的平方根?在这种明显错误的情况下会发生什么?您的函数能否处理这种愚蠢的行为?
看看真相。重新运行该场景,这次从 100 万作为初始 bestGuess 开始

图 5. 通过从 1,000,000 作为初始 bestGuess 开始计算 25 的平方根时的逐步改进。
哇!从一个荒谬的巨大数字开始,迭代次数仅增加了两倍(从 8 次迭代到 23 次)。远没有您可能直观预期的那么剧烈增加。
故事的寓意
当您意识到不仅迭代保证可以解决问题,而且无论您对解决方案的搜索是从一个好的还是糟糕的初始猜测开始都无关紧要时,顿悟时刻就到来了。无论您的初始理解多么错误,迭代过程,加上可衡量的测试/目标,都会将您引向正确的方向并交付解决方案。有保证的。
图 4 和图 5 显示了一个陡峭而引人注目的燃尽图。从一个非常不正确的起点开始,迭代迅速燃尽到绝对正确的解决方案。
简而言之,这种令人惊叹的方法论是敏捷 DevOps 的精髓。
回到一些高层次的观察
敏捷 DevOps 实践源于我们生活在一个从根本上基于不确定性、模糊性、不完整性和一定程度的混乱的世界的认识。从科学/哲学的角度来看,这些特征已得到充分记录,并得到 海森堡不确定性原理(涵盖不确定性部分)、维特根斯坦的《逻辑哲学论》(模糊性部分)、哥德尔不完备性定理(不完备性方面)和 热力学第二定律(无情的熵造成的混乱)的支持。
简而言之,无论您多么努力,在尝试解决任何问题时,您都永远无法获得完整的信息。因此,放弃傲慢的姿态,采取更谦逊的方法来解决问题更有利可图。谦逊会带来丰厚的回报,不仅回报您期望的解决方案,还回报结构良好的解决方案的副产品。
结论
自然界不知疲倦地运作——这是一个持续的流动。自然界没有总体规划;一切的发生都是对先前发生的事情的回应。反馈循环非常紧密,明显的进步/倒退是零星的。在自然界的任何地方,您都会看到一种或另一种形式的逐步改进。
敏捷 DevOps 是工程模型逐步成熟的一个非常有趣的结果。DevOps 基于这样的认识:您可用的信息始终是不完整的,因此您最好谨慎行事。获得一个可衡量的测试(例如,一个假设,一个可衡量的期望),谦虚地尝试满足它,很可能会失败,然后收集反馈,修复失败,然后继续。除了同意在每一步都必须有一个可衡量的假设/测试之外,没有其他计划。
在本系列的下一篇文章中,我将更仔细地研究突变测试如何提供驱动价值的急需的反馈。
9 条评论