蚊章汇 - Alex Bunardzic
type: channel
link: https://linuxcn.bwsl.wang/author/1312/
description: Alex Bunardzic
items: ZOMBIES：为什么简洁性是交付健壮软件的关键（五）
items: ZOMBIES：如何在软件开发中实现业务需求（四）
items: ZOMBIES：在软件开发中定义边界和接口（三）
items: ZOMBIES：软件开发和测试中的构建与拓展（二）
items: ZOMBIES：我的软件开发和测试简便指南（一）
items: 变异测试：基于故障的试验
items: 变异测试：如何利用故障？
items: 变异测试是测试驱动开发（TDD）的演变

ZOMBIES：为什么简洁性是交付健壮软件的关键（五）
type: item
date: 2023-06-05T23:34:04+08:00
description: 当你坚持最简场景时，你最终会得到最简单的解决方案。在前面的文章中，我已经解释了为什么将编程问题看作一整群丧尸来处理是错误的。我用 ZOMBIES 方法来解释为什么循序渐进地处理问题更好。ZOMBIES 表示以下首字母缩写：Z – 最简场景（Zero）O – 单元素场景（One）M – 多元素场景（Many or more complex）B – 边界行为（Boundary behaviors）I – 接口定义（Interface definition）E – 处理特殊行为（Exercise exceptional behavior）S – 简单场景用简单的解决方案（Simple scenarios, simple solutions）在系列的前四篇文章中，我展示了 ZOMBIES 方法的前六个原则（LCTT译注：原文为前五个，应为笔误）。第一篇中 实现了最简场景，它为代码提供了最简可行路径。第二篇文章中执行了 单元素场景和多元素场景上的测试。第三篇中介绍了 边界和接口。 第四篇中处理 特殊行为。在本文中，我将介绍最后一项：简单场景用简单的解决方案。简单场景用简单的解决方案回顾这个网购 API 的实现过程，你会发现总是有目的性地坚持考虑最简单的场景。在这个过程中，最终你会得到最简单的解决方案。ZOMBIES 方法通过坚持简洁性来帮助你交付健壮优雅的解决方案。胜利了吗?似乎一切工作都结束了，一个不那么认真负责的工程师很可能会宣布胜利。但开明的工程师总是会探索得更深一点。我一直推荐做 变异测试 mutation testing 。在圆满结束这个练习项目，开始新的征程之前，用变异测试来敲打敲打你的解决方案是明智的。况且你也不得不承认，变异很适合对付丧尸的。你可以在开源网站 Stryker.NET 上进行变异测试。看起来有一个存活的变异体。这可不是好兆头。这意味着什么呢？当你自认为解决方案无懈可击的时候，Stryker.NET 却表示在你的地盘上并非一切安好。让我们来看看这个存活下来的烦人变异体：变异测试工具将if(total > 500.00) { 变异为：if(total >= 500.00) { 然后运行测试，结果对于这个变化没有一个测试失败。如果业务处理代码中发生了一处变动却没有任何一个测试失败，这就意味着你遇到一个存活的变异体。为什么要在意变异体为什么存活的变异体是麻烦的征兆呢？因为你写的业务处理逻辑代码控制着整个系统的行为。如果业务处理逻辑改变，系统的行为也应该随之改变。而系统行为的改变应该会导致测试表示的期望被违反。如果没有期望被违反，那就说明这些期望对系统行为的描述还不够准确。这也意味着你的业务处理逻辑中存在漏洞。要解决这个问题，你需要干掉这个存活下来的变异体。要怎么做呢？一般来说，有存活的变异体意味着有期望被遗漏了。仔细检查代码，梳理已定义的期望，看看漏掉了什么：期望 0：新建购物框里有零个商品（这隐含了总价为 ￥0）。期望 1：向购物框添加一件商品的结果是购物框里有一件商品，如果这件商品的价格是 ￥10，那么总价为 ￥10。期望 2：向购物框添里加入一件价值 ￥10 的商品，再加入一件价值 ￥20 的商品，总价是 ￥30 。期望 3：关于从购物框移除商品的期望。期望 4：总价大于 ￥500 时打，享受九折优惠。缺了什么呢？根据变异测试报告，你没有定义订单总价刚好为 ￥500 的销售策略。你已经定义订单总额大于 ￥500 和小于 ￥500 时的情况。定义边界情况的期望：[Fact] public void Add2ItemsTotal500GrandTotal500() { var expectedGrandTotal = 500.00; var actualGrandTotal = 450; Assert.Equal(expectedGrandTotal, actualGrandTotal); } 第一步先写一个假的实现让测试失败。现在共有 9 个微测试。其中 8 个通过，第 9 个失败了：[xUnit.net 00:00:00.57] tests.UnitTest1.Add2ItemsTotal500GrandTotal500 [FAIL] X tests.UnitTest1.Add2ItemsTotal500GrandTotal500 [2ms] Error Message: Assert.Equal() Failure Expected: 500 Actual: 450 [...] Test Run Failed. Total tests: 9 Passed: 8 Failed: 1 Total time: 1.5920 Seconds 将硬编码值替换成正样例的预期代码：[Fact] public void A…

ZOMBIES：如何在软件开发中实现业务需求（四）
type: item
date: 2023-05-30T17:43:56+08:00
description: 完善你的电商应用，使它能够正确处理业务规则。在前面的文章中，我已经解释了为什么将编程问题看作一整群丧尸来处理是错误的。我用 ZOMBIES 方法来解释为什么循序渐进地处理问题更好。ZOMBIES 表示以下首字母缩写：Z – 最简场景（Zero）O – 单元素场景（One）M – 多元素场景（Many or more complex）B – 边界行为（Boundary behaviors）I – 接口定义（Interface definition）E – 处理特殊行为（Exercise exceptional behavior）S – 简单场景用简单的解决方案（Simple scenarios, simple solutions）在系列的前三篇文章中，我展示了 ZOMBIES 方法的前五项。第一篇中 实现了最简场景，它为代码提供了最简可行路径。第二篇文章中执行了 单元素场景和多元素场景上的测试。第三篇中介绍了 边界和接口。在本文中，我将带你了解倒数第二个方法：处理特殊行为。处理特殊行为在开发一个电子购物应用时，你需要从产品负责人或赞助商那里了解需要采用什么销售策略。毫无疑问，与任何电子商业活动一样，你需要通过制定销售策略来诱导顾客进行消费。假设有如下的销售策略：订单金额超过 ￥500 时可以享受一定的折扣优惠。现在将这个销售策略转换为可运行期望：[Fact] public void Add2ItemsTotal600GrandTotal540() { var expectedGrandTotal = 540.00; var actualGrandTotal = 0.00; Assert.Equal(expectedGrandTotal, actualGrandTotal); } 这个正面样例表示的销售策略是，如果订单总额为 ￥600.00，那么 shoppingAPI 会将其减价为 ￥540.00。上面的代码伪造了一个失败验证用例。现在修改它，让它能够通过测试：[Fact] public void Add2ItemsTotal600GrandTotal540() { var expectedGrandTotal = 540.00; Hashtable item = new Hashtable(); item.Add("00000001", 200.00); shoppingAPI.AddItem(item); Hashtable item2 = new Hashtable(); item2.Add("00000002", 400.00); shoppingAPI.AddItem(item2); var actualGrandTotal = shoppingAPI.CalculateGrandTotal(); Assert.Equal(expectedGrandTotal, actualGrandTotal); } 在这个正样例中，你向购物框加入一件价值 ￥200 的商品和一件价值 ￥400 的商品，使总价达到 ￥600 。当调用 CalculateGrandTotal() 方法时，你期望总价是 ￥540。这个微测试能够通过吗？[xUnit.net 00:00:00.57] tests.UnitTest1.Add2ItemsTotal600GrandTotal540 [FAIL] X tests.UnitTest1.Add2ItemsTotal600GrandTotal540 [2ms] Error Message: Assert.Equal() Failure Expected: 540 Actual: 600 [...] 很可惜，它失败了。你期望的结果是 ￥540，但计算结果为 ￥600。为什么会这样呢？那是因为你还没有告诉系统在订单总价大于 ￥500 时怎么进行折扣计算。现在来实现折扣计算逻辑。根据上面的正样例可知，当订单总价为 ￥600（超过了营销策略的阈值 ￥500）时，期望的最终总价为 ￥540。也就是说系统需要从订单总额中减去 ￥60。刚好是是原订单总价的 10%。因此该销售规则就是当订单总额超过 ￥500 时享受九折优惠。在 ShippingAPI 类中实现该处理逻辑：private double Calculate10PercentDiscount(double total) { double discount = 0.00; if(total > 500.00) { discount = (total/100) * 10; } return discount; } 首先，检查订单总额是否大于 ￥500 。如果是，则计算出总额的 10%。你还需要告诉系统怎么从订单总额中减去 10%。改动非常直接：return g…

ZOMBIES：在软件开发中定义边界和接口（三）
type: item
date: 2023-05-30T17:22:00+08:00
description: 丧尸是没有边界感的，需要为你的软件设定限制和期望。丧尸没有边界感。它们踩倒栅栏，推倒围墙，进入不属于它们的地盘。在前面的文章中，我已经解释了为什么把所有编程问题当作一群丧尸一次性处理是错误的。ZOMBIES 代表首字母缩写：Z – 最简场景（Zero）O – 单个元素场景（One）M – 多个元素场景（Many or more complex）B – 边界行为（Boundary behaviors）I – 接口定义（Interface definition）E – 处理特殊行为（Exercise exceptional behavior）S – 简单场景用简单的解决方案（Simple scenarios, simple solutions）在本系列的前面两篇文章中，我演示了 ZOMBIES 方法的前三部分：最简场景、单元素场景和多元素场景。第一篇文章 实现了最简场景，它提供了代码中的最简可行路径。第二篇文章中针对单元素场景和多元素场景 运行测试。在这篇文章中，我将带你了解边界和接口。回到单元素场景要想处理边界，你需要绕回来（迭代）。首先思考下面的问题：电子商务的边界是什么？我需要限制购物框的大小吗？（事实上，我不认为这有任何意义。）目前唯一合理的边界条件是确保购物框里的商品数量不能为负数。将这个限制表示成可运行的期望：[Fact] public void Add1ItemRemoveItemRemoveAgainHas0Items() { var expectedNoOfItems = 0; var actualNoOfItems = -1; Assert.Equal(expectedNoOfItems, actualNoOfItems); } 这就是说，如果你向购物框里添加一件商品，然后将这个商品移除两次，shoppingAPI 的实例应该告诉你购物框里有零个商品。当然这个可运行期望（微测试）不出意料地会失败。想要这个微测试能够通过，最小改动是什么呢？[Fact] public void Add1ItemRemoveItemRemoveAgainHas0Items() { var expectedNoOfItems = 0; Hashtable item = new Hashtable(); shoppingAPI.AddItem(item); shoppingAPI.RemoveItem(item); var actualNoOfItems = shoppingAPI.RemoveItem(item); Assert.Equal(expectedNoOfItems, actualNoOfItems); } 这个期望测试依赖于 RemoveItem(item) 功能。目前的 shippingAPI 还不具备该功能，你需要增加该功能。回到 app 文件夹，打开 IShippingAPI.cs 文件，新增以下声明：int RemoveItem(Hashtable item); 到 ShippingAPI.cs 中实现该功能：public int RemoveItem(Hashtable item) { basket.RemoveAt(basket.IndexOf(item)); return basket.Count; } 运行，然后你会得到如下错误：系统在移除一个不在购物框的商品，这导致了系统崩溃。加一点点 防御式编程defensive programming：public int RemoveItem(Hashtable item) { if(basket.IndexOf(item) >= 0) { basket.RemoveAt(basket.IndexOf(item)); } return basket.Count; } 在移除商品之前先检查它是否在购物框中。（你可能试过用捕获异常的方式来处理，但是我认为上面的处理方式更具可读性。）更多具体的期望在讲更多具体的期望之前，让我们先探讨一下什么是接口。在软件工程中，接口表示一种规范，或者对能力的描述。从某种程度上来说，接口类似于菜谱。它罗列出了制作蛋糕的原材料，但它本身并不能吃。我们只是按照菜谱上的说明来烤蛋糕。与此类似，我们首先通过说明这个服务能做什么的方式来定义我们的服务。这个描述说明就是所谓的接口。但是接口本身并不能向我们提供任何功能。它只是指导我们实现指定功能的蓝图而已。到目前为止，我们已经实现了接口（只是某部分实现了，稍后还会增加新功能）和业务处理边界（也就是购物框里的商品不能是负数）。你指导了 shoppingAPI 怎么向购物框添加商品，并通过 Add2ItemsBasketHas2Items 测试验证了该功能的有效性。然而仅仅具备向购物框添加商品的功能还不足以…

ZOMBIES：软件开发和测试中的构建与拓展（二）
type: item
date: 2023-05-16T23:50:41+08:00
description: 在开发初期只对单个元素进行编码和测试，之后再拓展到多个元素上。在 上一篇文章 中我已经解释了为什么把所有编程问题当作一群丧尸一次性处理是错误的。我也解释了 ZOMBIES 方法中的第一条：最简场景。本文中我将进一步介绍接下来的两条：单元素场景和多元素场景。ZOMBIES 表示以下首字母缩写：Z – 最简场景（Zero）O – 单元素场景（One）M – 多元素场景（Many or more complex）B – 边界行为（Boundary behaviors）I – 接口定义（Interface definition）E – 处理特殊行为（Exercise exceptional behavior）S – 简单场景用简单的解决方案（Simple scenarios, simple solutions）在上一篇文章中，通过应用了最简场景，你在代码里构建了一条最简可行通路。这个代码里没有任何业务处理逻辑。现在是时候向系统中添加一个元素了。最简场景表示系统中什么也没有，这是一个空的用例，我们什么也不用关心。单元素场景代表我们有一个元素需要关心考虑。这个单一元素可能是集合中的一个元素、一个访问着或者一个需要处理的事件。对于多元素场景，我们需要处理更复杂的情况，比如两个或更多的集合元素或事件。单元素场景在上一篇文章的代码基础上，向虚拟购物筐里添加一些商品。首先，写一个伪测试：[Fact] public void Add1ItemBasketHas1Item() { var expectedNoOfItems = 1; var actualNoOfItems = 0; Assert.Equal(expectedNoOfItems, actualNoOfItems); } 不出所料，这个测试失败了，因为硬编码了一个错误的值：Starting test execution, please wait... A total of 1 test files matched the specified pattern. [xUnit.net 00:00:00.57] tests.UnitTest1.NewlyCreatedBasketHas0Items [FAIL] X tests.UnitTest1.NewlyCreatedBasketHas0Items [4ms] Error Message: Assert.Equal() Failure Expected: 0 Actual: 1 [...] 现在是时候停止伪造了。现在你已经用 ArrayList 实现了购物筐。那么应该怎么实现商品呢？简洁性应该一直是你的指导原则。在不了解商品的太多信息的情况下，你可以先用另一个集合来实现它。这个表示商品的集合应该包含些什么呢？由于你多半会关心计算购物筐中的商品总价，所以对商品的表示至少需要包含价格（可以是任意货币，为简单起见，不妨假设是人民币）。（我们需要）一个简单的集合类型，它包含一个商品 ID（可以在系统中的其它地方使用 ID 来指向该商品）和这个商品的价格。键值对类型的数据结构可以很容易满足这个需求。在 C# 中最先被想到的数据结构就是 Hashtable。在购物应用的代码中给 IShoppingAPI 增加一个新功能：int AddItem(Hashtable item); 这个新功能以一个用 Hashtable 表示的商品为输入，返回购物筐中的商品数量。将测试代码中硬编码的值提替换为对接口的调用：[Fact] public void Add1ItemBasketHas1Item() { var expectedNoOfItems = 1; Hashtable item = [new][3] Hashtable(); var actualNoOfItems = shoppingAPI.AddItem(item); Assert.Equal(expectedNoOfItems, actualNoOfItems); } 在上面的代码中实例化了一个 Hashtable 并命名为 item，然后调用购物接口中的 AddItem(item) 方法，该方法会返回购物筐中实际的商品数量。转到 ShoppingAPI 类中，实现这个方法：public int AddItem(Hashtable item) { return 0; } 这里再次通过写假代码来检验测试的效果（测试是业务代码的第一个调用者）。如果测试失败，将硬编码值换成实际的代码：public int AddItem(Hashtable item) { basket.Add(item); return basket.Count; } 在上面的代码中，向购物筐里添加了一件商品，然后返回购物筐中的商品数量：T…

ZOMBIES：我的软件开发和测试简便指南（一）
type: item
date: 2023-05-13T21:52:10+08:00
description: 编程过程有时候就像一场与丧尸群之间的战斗。在这个系列文章中，我将带你了解怎样将 ZOMBIES 方法应用到实际工作中。很久以前，在我还是一个萌新程序员的时候，我们曾经被分配一大批工作。我们每个人都被分配了一个编程任务，然后回到自己的小隔间里噼里啪啦地敲键盘。我记得团队里的成员在自己的小隔间里一呆就是几个小时，为打造无缺陷的程序而奋斗。当时流行的思想是：能一次性做得越多，能力越强。对于我来说，能够长时间编写或者修改代码而不用中途停下来检验这些代码是否有效，就像荣誉勋章一样。那个时候我们都认为停下来检验代码是否工作是能力不足的表现，菜鸟才这么干。一个“真正的开发者”应该能一口气构建起整个程序，中途不用停下来检查任何东西！然而事与愿违，当我停止在开发过程中测试自己的代码之后，来自现实的检验狠狠地打了我的脸。我的代码要么无法通过编译，要么构建失败，要么无法运行，或者不能按预期处理数据。我不得不在绝望中挣扎着解决这些烦人的问题。避开丧尸群如果你觉得旧的工作方式听起来很混乱，那是因为它确实是这样的。我们一次性处理所有的任务，在问题堆里左砍右杀，结果只是引出更多的问题。着就像是跟一大群丧尸间的战斗。如今我们已经学会了避免一次性做太多的事情。在最初听到一些专家推崇避免大批量地开发的好处时，我觉得这很反直觉，但我已经从过去的犯错中吸取了教训。我使用被 James Grenning 称为 ZOMBIES 的方法来指导我的软件开发工作。ZOMBIES 方法来救援!ZOMBIES 表示以下首字母缩写：Z – 最简场景（Zero）O – 单元素场景（One）M – 多元素场景（Many or more complex）B – 边界行为（Boundary behaviors）I – 接口定义（Interface definition）E – 处理特殊行为（Exercise exceptional behavior）S – 简单场景用简单的解决方案（Simple scenarios, simple solutions）我将在本系列文章中对它们进行分析讲解。最简场景最简场景指可能出现的最简单的情况。人们倾向于最开始的时候使用硬编码值，因为这是最简单的方式。通过在编码活动中使用硬编码值，可以快速构建出一个能即时反馈的解决方案。不需要几分钟，更不用几个小时，使用硬编码值让你能够马上与正在构建的系统进行交互。如果你喜欢这个交互，就朝这个方向继续做下去。如果你发现不喜欢这种交互，你可以很容易抛弃它，根本没有什么可损失。本系列文章将以构建一个简易的购物系统的后端 API 为例进行介绍。该服务提供的 API 允许用户创建购物筐、向购物筐添加商品、从购物筐移除商品、计算商品总价。首先，创建项目的基本结构（将购物程序的代码和测试代码分别放到 app 和 tests 目录下）。我们的例子中使用开源的 xUnit 测试框架。现在撸起你的袖子，在实践中了解最简场景吧！[Fact] public void NewlyCreatedBasketHas0Items() { var expectedNoOfItems = 0; var actualNoOfItems = 1; Assert.Equal(expectedNoOfItems, actualNoOfItems); } 这是一个伪测试，它测试的是硬编码值。新创建的购物筐是空的，所以购物筐中预期的商品数是 0。通过比较期望值和实际值是否相等，这个预期被表示成一个测试（或者称为断言）。运行该测试，输出结果如下：Starting test execution, please wait... A total of 1 test files matched the specified pattern. [xUnit.net 00:00:00.57] tests.UnitTest1.NewlyCreatedBasketHas0Items [FAIL] X tests.UnitTest1.NewlyCreatedBasketHas0Items [4ms] Error Message: Assert.Equal() Failure Expected: 0 Actual: 1 [...] 这个测试显然无法通过：期望商品数是 0，但是实际值被硬编码为了 1。当然，你可以马上把硬编码的值从 1 改成 0，这样测试就能通过了：[Fact] public void NewlyCreatedBasketHas0Items() { var expectedNoOfItems = 0; var actualNoOfItems = 0; Assert.Equal(expectedNoOfItems, actualNoOfItems); } 与预想的一样，运行测试…

变异测试：基于故障的试验
type: item
date: 2019-10-24T14:00:00+08:00
description: 基于 .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 方法的实现是不可测试的！看看我们面临的核心挑战：GetDayOrNight 依赖隐藏输入。dayOrNight 的值取决于隐藏输入（它从内置系统时钟中获取一天的时间值）。GetDayOrNight 包含非确定性行为。从系统时钟中获取到的时间值是不确定的。（因为）该时间取决于你运行代码的时间点，而这一点我们认为这是不可预测的。GetDayOrNight API 的质量差。该 API 与具体的数据源（系统 DateTime）紧密耦合。GetDayOrNight 违反了单一责任原则。该方法实现同时使用和处理信息。优良作法是一种方法应负责执行一项职责。GetDayOrNight 有多个更改原因。可以想象内部时间源可能会更改的情况。同样，很容易想象处理逻辑也将改变。这些变化的不同原因必须相互隔离。当（我们）尝试了解 GetDayOrNight 行为时，会发现它的 API 签名不足。最理想的做法就是通过简单的查看 API 的签名，就能了解 API 预期的行为类型。GetDayOrNight 取决于全局共享可变状态。要不惜一切代价避免共享的可变状态！即使在阅读源代码之后，也无法预测 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 时间添加到 GetDayOrNig…

变异测试：如何利用故障？
type: item
date: 2019-10-21T15:57:00+08:00
description: 使用事先设计好的故障以确保你的代码达到预期的结果，并遵循 .NET xUnit.net 测试框架来进行测试。在变异测试是 TDD 的演变 一文中，我谈到了迭代的力量。在可度量的测试中，迭代能够保证找到问题的解决方案。在那篇文章中，我们讨论了迭代法帮助确定实现计算给定数字平方根的代码。我还演示了最有效的方法是找到可衡量的目标或测试，然后以最佳猜测值开始迭代。正如所预期的，第一次测试通常会失败。因此，必须根据可衡量的目标或测试对失败的代码进行完善。根据运行结果，对测试值进行验证或进一步加以完善。在此模型中，学习获得解决方案的唯一方法是反复失败。这听起来有悖常理，但它确实有效。按照这种分析，本文探讨了在构建包含某些依赖项的解决方案时使用 DevOps 的最佳方法。第一步是编写一个预期结果失败的用例。依赖性问题是你不能依赖它们正如 迈克尔·尼加德 Michael Nygard 在《没有终结状态的架构》中机智的表示的那样，依赖问题是一个很大的话题，最好留到另一篇文章中讨论。在这里，你将会看到依赖项给项目带来的一些潜在问题，以及如何利用测试驱动开发（TDD）来避免这些陷阱。首先，找到现实生活中的一个挑战，然后看看如何使用 TDD 解决它。谁把猫放出来？在敏捷开发环境中，通过定义期望结果开始构建解决方案会很有帮助。通常，在 用户故事 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 文件夹中创建一个新文件，并将其命名为 DayOrNightUtilit…

变异测试是测试驱动开发（TDD）的演变
type: item
date: 2019-10-17T17:44:35+08:00
description: 测试驱动开发技术是根据大自然的运作规律创建的，变异测试自然成为 DevOps 演变的下一步。在 “故障是无懈可击的开发运维中的一个特点”，我讨论了故障在通过征求反馈来交付优质产品的过程中所起到的重要作用。敏捷 DevOps 团队就是用故障来指导他们并推动开发进程的。 测试驱动开发 Test-driven development （TDD）是任何敏捷 DevOps 团队评估产品交付的必要条件。以故障为中心的 TDD 方法仅在与可量化的测试配合使用时才有效。TDD 方法仿照大自然是如何运作的以及自然界在进化博弈中是如何产生赢家和输家为模型而建立的。自然选择1859 年， 查尔斯·达尔文 Charles Darwin 在他的《 物种起源 On the Origin of Species 》一书中提出了进化论学说。达尔文的论点是，自然变异是由生物个体的自发突变和环境压力共同造成的。环境压力淘汰了适应性较差的生物体，而有利于其他适应性强的生物的发展。每个生物体的染色体都会发生变异，而这些自发的变异会携带给下一代（后代）。然后在自然选择下测试新出现的变异性 —— 当下存在的环境压力是由变异性的环境条件所导致的。这张简图说明了调整适应环境条件的过程。图1. 不同的环境压力导致自然选择下的不同结果。图片截图来源于理查德·道金斯的一个视频。该图显示了一群生活在自己栖息地的鱼。栖息地各不相同（海底或河床底部的砾石颜色有深有浅），每条鱼长的也各不相同（鱼身图案和颜色也有深有浅）。这张图还显示了两种情况（即环境压力的两种变化）：捕食者在场捕食者不在场在第一种情况下，在砾石颜色衬托下容易凸显出来的鱼被捕食者捕获的风险更高。当砾石颜色较深时，浅色鱼的数量会更少一些。反之亦然，当砾石颜色较浅时，深色鱼的数量会更少。在第二种情况下，鱼完全放松下来进行交配。在没有捕食者和没有交配仪式的情况下，可以预料到相反的结果：在砾石背景下显眼的鱼会有更大的机会被选来交配并将其特性传递给后代。选择标准变异性在进行选择时，绝不是任意的、反复无常的、异想天开的或随机的。选择过程中的决定性因素通常是可以度量的。该决定性因素通常称为测试或目标。一个简单的数学例子可以说明这一决策过程。（在该示例中，这种选择不是由自然选择决定的，而是由人为选择决定。）假设有人要求你构建一个小函数，该函数将接受一个正数，然后计算该数的平方根。你将怎么做？敏捷 DevOps 团队的方法是快速验证失败。谦虚一点，先承认自己并不真的知道如何开发该函数。这时，你所知道的就是如何描述你想做的事情。从技术上讲，你已准备好进行单元测试。“ 单元测试 unit test ”描述了你的具体期望结果是什么。它可以简单地表述为“给定数字 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如果有必要的话，为你的代码创建一个脚手架：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); } } }该单元测试描述了变量的期望值应该为 4。下一行描述了实际值。建议通过将输入值发送到称为…