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