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）在系列的前四篇文章中，我展示了 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 Add2ItemsTotal500GrandTotal500() { var expectedGrandTotal = 500.00; Hashtable item1 = new Hashtable(); item1.Add("0001", 400.00); shoppingAPI.AddItem(item1); Hashtable item2 = new Hashtable(); item2.Add("0002", 100.00); shoppingAPI.AddItem(item2); var actualGrandTotal = shoppingAPI.CalculateGrandTotal(); } 共添加了两件商品，一件价值 ￥400，另一件价值 ￥100，总价是 ￥500。调用计算总价的函数，期望的总价是 ￥500。运行，9 个测试全部通过！Total tests: 9 Passed: 9 Failed: 0 Total time: 1.0440 Seconds 现在是揭晓真相的时刻。这个新增的期望能够清理掉所有的变异体吗？运行变异测试来看看结果：成功了！10 个变异体全都被干掉了。太棒了！现在你可以放心地发布这个 API 了。结语如果从这次练习中有什么收获的话，那就是 技术性拖延 skillful procrastination 这一概念的提出。这个是一个重要的概念，因为我们中的许多人往往会在客户描述完他们的问题之前，就盲目地去设想解决方案。主动拖延拖延对于软件工程师来说并不是一件容易的事情。我们总是急于动手写代码。我们熟悉各种设计模式、反模式、编程原则和现成的解决方案。我们总是迫不及待想将它们应用到可执行的代码中，并且倾向于一次性做大量的工作。所以在行动之前仔细考虑每个步骤是一种美德。这个练习说明 ZOMBIES 方法能够通过一系列深思熟虑的小步骤来帮你实现解决方案。但是有一点需要特别注意，根据 Yagni 原则，这些深思熟虑常常会飞得太远，以至于最终形成一个大而全的系统。这会产生臃肿、紧密耦合的系统。迭代式开发与增量式开发在这次练习给我们的另一个重要收获是让我们意识到保持系统持续可用的唯一方法是采用迭代式的开发方法。你通过改造已有代码开发出整个购物 API。这种改造工作是在迭代优化解决方案的过程中不可避免的。很多团队混淆了迭代和增量。这是两个完全不同的概念。增量式方法是假设是你有完整清晰的需求，然后通过增量累加的方式来构建解方案。大体上来说，你一点点地构建各个部分，然后将所有的部分组装在一起。大功告成！解决方案已经准备好交付了!相比之下，迭代式方法中，你并不很确定自己需要交付给客户的是什么。因为这个原因，你更加小心谨慎。你会小心翼翼地避免破坏能够运行的系统（也就是说系统处于稳态）。如果不得不扰动已有的平衡，你也会采取最小侵入性的方式。你专注于用尽可能小的工作量来快速完成每次得改动工作。你倾向于让系统尽快回到稳态。这就是为什么迭代式方法在真正实现一个功能之前总是先提供一个假实现。你硬编码一系列的期望，以验证小的修改不会导致系统无法运行。然后进行必要的修改，用实际处理代码替换硬编码的值。作为经验法则，在迭代方法中，你的目标是通过设计期望（微测试），对代码进行不断改进。每进行一次改进，你都要检验系统，以确保它处于工作状态。以这种方式不断前进，最终会达到满足所有期望的程度，并且此时代码已经被重构，没有任何存活的变异体了。一旦达到了这种状态，你就可以相当自信地交付解决方案了。由衷感谢 Kent Beck、 Ron Jeffries 和GeePaw Hill 在我的软件工程学习道路的启发。愿 ZOMBIES 方法在软件开发的征程上帮助到你。（题图：MJ/ca463fc6-021b-4818-ba3d-9cd3c8736577）via: https://opensource.com/article/21/2/simplicity作者：Alex Bunardzic 选题：lkxed 译者：toknow-gh 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
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）在系列的前三篇文章中，我展示了 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 grandTotal - Calculate10PercentDiscount(grandTotal); 到此，所有测试都能够通过。你又一次享受到系统处于稳态的欢愉。你的代码通过处理特殊行为实现了需要的销售策略。最后一步现在我已经介绍完 ZOMBIE 了，只剩下 S 了。我将会在最后一篇中介绍它。（题图：MJ/7f8bf5d2-54ce-4d6e-9dbf-13abf6df966a）via: https://opensource.com/article/21/2/exceptional-behavior作者：Alex Bunardzic 选题：lujun9972 译者：toknow-gh 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
丧尸是没有边界感的，需要为你的软件设定限制和期望。丧尸没有边界感。它们踩倒栅栏，推倒围墙，进入不属于它们的地盘。在前面的文章中，我已经解释了为什么把所有编程问题当作一群丧尸一次性处理是错误的。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 测试验证了该功能的有效性。然而仅仅具备向购物框添加商品的功能还不足以使其成为一个网购应用程序。它还需要能够计算购物框里的商品的总价。现在需要增加另一个期望。按照惯例，从最直接明了的期望开始。当你向购物框里加入一件价值 ￥10 的商品时，你希望这个购物 API 能正确地计算出总价为 ￥10。第五个测试（伪造版）如下：[Fact] public void Add1ItemPrice10GrandTotal10() { var expectedTotal = 10.00; var actualTotal = 0.00; Assert.Equal(expectedTotal, actualTotal); } 还是一样的老把戏，通过硬编码一个错误的值让 Add1ItemPrice10GrandTotal10 测试失败。当然前三个测试成功通过，但第四个新增的测试失败了：A total of 1 test files matched the specified pattern. [xUnit.net 00:00:00.57] tests.UnitTest1.Add1ItemPrice10GrandTotal10 [FAIL] X tests.UnitTest1.Add1ItemPrice10GrandTotal10 [4ms] Error Message: Assert.Equal() Failure Expected: 10 Actual: 0 Test Run Failed. Total tests: 4 Passed: 3 Failed: 1 Total time: 1.0320 Seconds 将硬编码值换成实际的处理代码。首先，检查接口是否具备计算订单总价的功能。根本没有这种东西。目前为止接口中只声明了三个功能：int NoOfItems();int AddItem(Hashtable item);int RemoveItem(Hashtable item);它们都不具备计算总价的能力。所以需要声明一个新功能：double CalculateGrandTotal(); 这个新功能应该让 shoppingAPI 具备计算总价的能力。这是通过遍历购物框中的商品并把它们的价格累加起来实现的。修改第五个测试：[Fact] public void Add1ItemPrice10GrandTotal10() { var expectedGrandTotal = 10.00; Hashtable item = new Hashtable(); item.Add("00000001", 10.00); shoppingAPI.AddItem(item); var actualGrandTotal = shoppingAPI.CalculateGrandTotal(); Assert.Equal(expectedGrandTotal, actualGrandTotal); } 这个测试表明了这样的期望：如果向购物框里加入一件价格 ￥10 的商品，然后调用 CalculateGrandTotal() 方法，它会返回商品总价 ￥10。这是一个完全合理的期望，它完全符合商品总价计算的逻辑。那么怎么实现这个功能呢？就像以前一样，先写一个假的实现。回到 ShippingAPI 类中，实现在接口中声明的 CalculateGrandTotal() 方法：public double CalculateGrandTotal() { return 0.00; } 现在先将返回值硬编码为 0.00，只是为了检验这个测试能否正常运行，并确认它是能够失败的。事实上，它能够运行，并且如预期一样失败。接下来的工作就是正确实现计算商品总价的处理逻辑：public double CalculateGrandTotal() { double grandTotal = 0.00; foreach(var product in basket) { Hashtable item = product as Hashtable; foreach(var value in item.Values) { grandTotal += Double.Parse(value.ToString()); } } return grandTotal; } 运行，五个测试全部通过！从单元素场景到多元素场景现在是时候进入下一轮迭代了。你已经通过处理最简场景、单元素场景和边界场景迭代地构建了系统，现在需要处理稍复杂的多元素场景了。快捷提示：由于我们一直在针对单个元素场景、多元素场景和边界行为这三点上对软件进行迭代改进，一些读者可能会认为我们同样应该对接口进行改进。我们稍后就会发现，接口已经完全满足需要了，目前没有新增功能的必要。请记住，应该保持接口的简洁。（盲目地）扩增接口不会带来任何好处，只会引入噪音。我们要遵循 奥卡姆剃刀 Occam's Razor 原则：如无必要，勿增实体。 现在我们已经基本完成了接口功能描述的工作，是时候改进实现了。通过上一轮的迭代，系统已经能够处理购物框里有超过一件商品的情况了。现在我么来让系统具备购物框里有超过一件商品时计算总价的能力。首先写可执行期望：[Fact] public void Add2ItemsGrandTotal30() { var expectedGrandTotal = 30.00; var actualGrandTotal = 0.00; Assert.Equal(expectedGrandTotal, actualGrandTotal); } 硬编码所有值，尽量让期望测试失败。测试确实失败了，现在得想办法让它通过。向购物框添加两件商品，然后调用 CalculateGrandTotal() 方法：[Fact] public void Add2ItemsGrandTotal30() { var expectedGrandTotal = 30.00; Hashtable item = new Hashtable(); item.Add("00000001", 10.00); shoppingAPI.AddItem(item); Hashtable item2 = new Hashtable(); item2.Add("00000002", 20.00); shoppingAPI.AddItem(item2); var actualGrandTotal = shoppingAPI.CalculateGrandTotal(); Assert.Equal(expectedGrandTotal, actualGrandTotal); } 测试通过。现在共有六个可以通过的微测试，系统回到了稳态。设定期望作为一个认真负责的工程师，你希望确保当用户向购物框添加一些商品然后又移除一些商品后系统仍然能够计算出正确出总价。下面是这个新的期望：[Fact] public void Add2ItemsRemoveFirstItemGrandTotal200() { var expectedGrandTotal = 200.00; var actualGrandTotal = 0.00; Assert.Equal(expectedGrandTotal, actualGrandTotal); } 这个期望表示将两件商品加入到购物框，然后移除第一件后期望的总价是 ￥200。硬编码行为失败了。现在设计更具体的正面测试样例，然后运行代码：[Fact] public void Add2ItemsRemoveFirstItemGrandTotal200() { var expectedGrandTotal = 200.00; Hashtable item = new Hashtable(); item.Add("00000001", 100.00); shoppingAPI.AddItem(item); Hashtable item2 = new Hashtable(); item2.Add("00000002", 200.00); shoppingAPI.AddItem(item2); shoppingAPI.RemoveItem(item); var actualGrandTotal = shoppingAPI.CalculateGrandTotal(); Assert.Equal(expectedGrandTotal, actualGrandTotal); } 在这个正面测试样例中，先向购物框加入第一件商品（编号为 00000001，价格为 ￥100），再加入第二件商品（编号为 00000002，价格为 ￥200）。然后将第一件商品移除，计算总价，比较计算值与期望值是否相等。运行期望测试，系统正确地计算出了总价，满足这个期望测试。现在有七个能顺利通过的测试了。系统运行良好，无异常！Test Run Successful. Total tests: 7 Passed: 7 Total time: 0.9544 Seconds 敬请期待现在你已经学习了 ZOMBIES 方法中的 ZOMBI 部分，下一篇文章将介绍处理特殊行为。到那个时候，你可以试试自己的测试！（题图：MJ/c4eb23b5-84aa-4477-a6b9-7d2a6d1aeee4）via: https://opensource.com/article/21/2/boundaries-interfaces作者：Alex Bunardzic 选题：lujun9972 译者：toknow-gh 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
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）在上一篇文章中，通过应用了最简场景，你在代码里构建了一条最简可行通路。这个代码里没有任何业务处理逻辑。现在是时候向系统中添加一个元素了。最简场景表示系统中什么也没有，这是一个空的用例，我们什么也不用关心。单元素场景代表我们有一个元素需要关心考虑。这个单一元素可能是集合中的一个元素、一个访问着或者一个需要处理的事件。对于多元素场景，我们需要处理更复杂的情况，比如两个或更多的集合元素或事件。单元素场景在上一篇文章的代码基础上，向虚拟购物筐里添加一些商品。首先，写一个伪测试：[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; } 在上面的代码中，向购物筐里添加了一件商品，然后返回购物筐中的商品数量：Test Run Successful. Total tests: 2 Passed: 2 Total time: 1.0633 Seconds 到目前为止，你通过了两个测试，同时也基本里解了 ZOMBIES 方法中的最简场景和单元素场景两部分。反思总结回顾前面所做的工作，你会发现通过将注意力集中到处理最简场景和单元素场景上，你在构建接口的同时也定义了一些业务逻辑边界！这不是很棒吗？现在你已经部分地实现了最关键的抽象逻辑，并且能够处理什么也没有和只有一个元素的的情况。因为你正在构建的是一个电子交易 API，所以你不能对顾客的购物行为预设其它限制。总而言之，虚拟购物筐应该是无限大的。ZOMBIES 提供的逐步优化思路的另一个重要方面（虽然不是很明显）是从大概思路到具体实现的阻力。你也许已经注意到了，要具体实现某个东西总是困难重重。倒不如先用硬编码值来构造一个伪实现。只有看到接口与测试之间以一种合理的方式交互之后，你才会愿意开始完善实现代码。即便如此，你也应该采用简单直接的代码结构，尽可能避免条件逻辑分支。多元素场景通过定义顾客向购物筐里添加两件商品时的期望来拓展应用程序。首先构造一个伪测试。它的期望值为 2，但是现在将实际值硬编码为 0，强制让测试失败：[Fact] public void Add2ItemsBasketHas2Items() { var expectedNoOfItems = 2; var actualNoOfItems = 0; Assert.Equal(expectedNoOfItems, actualNoOfItems); } 执行测试，前两个测试用例通过了（针对最简场景和单元素场景的测试），而硬编码的测试不出所料地失败了：A total of 1 test files matched the specified pattern. [xUnit.net 00:00:00.57] tests.UnitTest1.Add2ItemsBasketHas2Items [FAIL] X tests.UnitTest1.Add2ItemsBasketHas2Items [2ms] Error Message: Assert.Equal() Failure Expected: 2 Actual: 0 Test Run Failed. Tatal tests: 3 Passed: 2 Failed: 1 将硬编码值替换为实际的代码调用：[Fact] public void Add2ItemsBasketHas2Items() { var expectedNoOfItems = 2; Hashtable item = [new][3] Hashtable(); shoppingAPI.AddItem(item); var actualNoOfItems = shoppingAPI.AddItem(item); Assert.Equal(expectedNoOfItems, actualNoOfItems); } 在这个测试中，你向购物筐中添加了两件商品（实际上是将同一件商品添加了两次），然后比较期望的商品数量和第二次添加商品后调用 shoppingAPI 返回的商品数量是否相等。现在所有测试都能够通过！敬请期待现在你已经了解了最简场景、单元素场景和多元素场景。我将下一篇文章中介绍边界行为和接口定义。敬请期待!（题图：MJ/e4679f1f-311a-4a41-80e8-8d2834b956f2）via: https://opensource.com/article/21/2/build-expand-software作者：Alex Bunardzic 选题：lujun9972 译者：toknow-gh 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
编程过程有时候就像一场与丧尸群之间的战斗。在这个系列文章中，我将带你了解怎样将 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); } 与预想的一样，运行测试，测试通过：Starting test execution, please wait... A total of 1 test files matched the specified pattern. Test Run Successful. Total tests: 1 Passed: 1 Total time: 1.0950 Seconds 你也许会认为执行一个被强迫失败的测试完全没有意义，但是不管一个测试多么简单，确保它的可失败性是绝对有必要的。只有这样才能够保证如果在后续工作中不小心破坏了程序的处理逻辑时该测试能够给你相应的警告。现在停止伪造数据，将硬编码的值替换成从 API 中获取的值。我们已经构造了一个能够可靠地失败的测试，它期望一个空的购物筐中有 0 个商品，现在是时候编写一些应用程序代码了。就跟常见的软件建模活动一样，我们先从构造一个简单的接口开始。在 app 目录下新建文件 IShoppingAPI.cs（习惯上接口名一般以大写 I 开头）。在该接口中声明一个名为 NoOfItems() 的方法，它以 int 类型返回商品的数量。下面是接口的代码：using System; namespace app { public interface IShoppingAPI { int NoOfItems(); } } 当然这个接口什么事也做不了，在你需要实现它。在 app 目录下创建另一个文件 ShoppingAPI。在其中将 ShoppingAPI 声明为一个实现了 IShoppingAPI 的公有类。在类中定义方法 NoOfItems 返回整数 1：using System; namespace app { public class ShoppingAPI : IShoppingAPI { public int NoOfItems() { return 1; } } } 从上面代码中你发现自己又在通过返回硬编码值 1 的方式来伪造代码逻辑。现阶段这是一件好事，因为你需要保持一切超级无敌简单。现在还不是仔细构想如何实现购物筐的处理逻辑时候。这些工作后续再做！到目前为止，你只是通过构建最简场景来检验自己是否满意现在的设计。为了确定这一点，将硬编码值换成这个 API 在运行中收到请求时应该返回的值。你需要通过 using app; 声明来告诉测试你使用的购物逻辑代码在哪里。接下来，你需要 实例化 instantiate IShoppingAPI 接口：IShoppingAPI shoppingAPI = new ShoppingAPI(); 这个实例用来发送请求并接收返回的值。现在，代码变成了这样：using System; using Xunit; using app; namespace tests { public class ShoppingAPITests { IShoppingAPI shoppingAPI = [new][3] ShoppingAPI(); [Fact] public void NewlyCreatedBasketHas0Items() { var expectedNoOfItems = 0; var actualNoOfItems = shoppingAPI.NoOfItems(); Assert.Equal(expectedNoOfItems, actualNoOfItems); } } } 显然执行这个测试的结果是失败，因为你硬编码了一个错误的返回值（期望值是 0，但是返回的是 1）。同样的，你也可以通过将硬编码的值从 1 改成 0 来让测试通过，但是现在做这个是在浪费时间。现在设计的接口已经跟测试关联上了，你剩下的职责就是编写代码实现预期的行为逻辑。在编写应用程序代码时，你得决定用来表示购物筐得数据结构。为了保持设计的简单，尽量选择 C# 中表示集合的最简单类型。第一个想到的就是 ArrayList。它非常适合目前的使用场景——可以保存不定个数的元素，并且易于遍历访问。因为 ArrayList 是 System.Collections 包的一部分，在你的代码中需要声明：using System.Collections; 然后 basket 的声明就变成这样了：ArrayList basket = new ArrayList(); 最后将 NoOfItems() 中的因编码值换成实际的代码：public int NoOfItems() { return basket.Count; } 这次测试能够通过了，因为最初购物筐是空的，basket.Count 返回 0。这也是你的第一个最简场景测试要做的事情。更多案例目前的课后作业是处理一个丧尸，也就是第 0 个丧尸。在下一篇文章中，我将带你了解单元素场景和多元素场景。不要错过哦！（题图：MJ/7917bc47-5325-4c0f-a2dd-4e444f57a46c）via: https://opensource.com/article/21/2/development-guide作者：Alex Bunardzic 选题：lujun9972 译者：toknow-gh 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
基于 .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 时间添加到 GetDayOrNight 方法，进一步重构代码。这是重构的类 DayOrNightUtility.cs：using System; namespace app { public class DayOrNightUtility { public string GetDayOrNight(DateTime time) { string dayOrNight = "Nighttime"; if(time.Hour >= 7 && time.Hour < 19) { dayOrNight = "Daylight"; } return dayOrNight; } } }重构代码需要更改单元测试。 需要准备 nightHour 和 dayHour 的测试数据，并将这些值传到GetDayOrNight 方法中。 以下是重构的单元测试：using System; using Xunit; using app; namespace unittest { public class UnitTest1 { DayOrNightUtility dayOrNightUtility = new DayOrNightUtility(); DateTime nightHour = new DateTime(2019, 08, 03, 19, 00, 00); DateTime dayHour = new DateTime(2019, 08, 03, 07, 00, 00); [Fact] public void Given7pmReturnNighttime() { var expected = "Nighttime"; var actual = dayOrNightUtility.GetDayOrNight(nightHour); Assert.Equal(expected, actual); } [Fact] public void Given7amReturnDaylight() { var expected = "Daylight"; var actual = dayOrNightUtility.GetDayOrNight(dayHour); Assert.Equal(expected, actual); } } }经验教训在继续开发这种简单的场景之前，请先回顾复习一下本次练习中所学到的东西。运行无法测试的代码，很容易在不经意间制造陷阱。从表面上看，这样的代码似乎可以正常工作。但是，遵循测试驱动开发（TDD）的实践（首先描述期望结果，然后才描述实现），暴露了代码中的严重问题。这表明 TDD 是确保代码不会太凌乱的理想方法。TDD 指出了一些问题区域，例如缺乏单一责任和存在隐藏输入。此外，TDD 有助于删除不确定性代码，并用行为明确的完全可测试代码替换它。最后，TDD 帮助交付易于阅读、逻辑易于遵循的代码。在本系列的下一篇文章中，我将演示如何使用在本练习中创建的逻辑来实现功能代码，以及如何进行进一步的测试使其变得更好。via: https://opensource.com/article/19/9/mutation-testing-example-failure-experimentation作者：Alex Bunardzic 选题：lujun9972 译者：Morisun029 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
使用事先设计好的故障以确保你的代码达到预期的结果，并遵循 .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 文件夹中创建一个新文件，并将其命名为 DayOrNightUtility.cs。将以下 C＃ 代码添加到该文件中并保存：using System; namespace app { public class DayOrNightUtility { public string GetDayOrNight() { string dayOrNight = "Undetermined"; return dayOrNight; } } }现在转到命令行，将目录更改为 unittests 文件夹，然后运行：[Xunit.net 00:00:02.33] unittest.UnitTest1.Given7pmReturnNighttime [FAIL] Failed unittest.UnitTest1.Given7pmReturnNighttime [...]恭喜，你已经完成了第一个失败的单元测试。单元测试的期望结果是 DayOrNightUtility 方法返回字符串 Nighttime，但相反，它返回是 Undetermined。修复失败的单元测试修复失败的测试的一种快速而粗略的方法是将值 Undetermined 替换为值 Nighttime 并保存更改：using System; namespace app { public class DayOrNightUtility { public string GetDayOrNight() { string dayOrNight = "Nighttime"; return dayOrNight; } } }现在运行，成功了。Starting test execution, please wait... Total tests: 1. Passed: 1. Failed: 0. Skipped: 0. Test Run Successful. Test execution time: 2.6470 Seconds但是，对值进行硬编码基本上是在作弊，最好为 DayOrNightUtility 方法赋予一些智能。修改 GetDayOrNight 方法以包括一些时间计算逻辑：public string GetDayOrNight() { string dayOrNight = "Daylight"; DateTime time = new DateTime(); if(time.Hour < 7) { dayOrNight = "Nighttime"; } return dayOrNight; }该方法现在从系统获取当前时间，并与 Hour 比较，查看其是否小于上午 7 点。如果小于，则处理逻辑将 dayOrNight 字符串值从 Daylight 转换为 Nighttime。现在，单元测试通过。测试驱动解决方案的开始现在，我们已经开始了基本的单元测试，并为我们的时间依赖项提供了可行的解决方案。后面还有更多的测试案例需要执行。在下一篇文章中，我将演示如何对白天时间进行测试以及如何在整个过程中利用故障。via: https://opensource.com/article/19/9/mutation-testing-example-tdd作者：Alex Bunardzic 选题：lujun9972 译者：Morisun029 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
测试驱动开发技术是根据大自然的运作规律创建的，变异测试自然成为 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。下一行描述了实际值。建议通过将输入值发送到称为calculator 的组件来计算实际值。对该组件的描述是通过接收数值来处理CalculateSquareRoot 信息。该组件尚未开发。但这并不重要，我们在此只是描述期望值。最后，描述了触发消息发送时发生的情况。此时，判断期望值是否等于实际值。如果是，则测试通过，目标达成。如果期望值不等于实际值，则测试失败。接下来，要实现称为 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 文件。现在，你可以运行第一个测试了。切换到命令行，进入 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. 单元测试通过了。迭代解决了问题正如大自然母亲解决问题的方法，在本练习中，迭代解决了问题。增量方法与逐步改进相结合是获得满意解决方案的有效方法。该示例中的决定性因素是具有可衡量的目标和测试。一旦有了这些，就可以继续迭代直到达到目标。关键点!好的，这是一个有趣的试验，但是更有趣的发现来自于使用这种新创建的解决方案。到目前为止，bestGuess 从开始一直把函数接收到的数字作为输入参数。如果更改 bestGuess 的初始值会怎样？为了测试这一点，你可以测试几种情况。 首先，在迭代多次尝试计算 25 的平方根时，要逐步细化观察结果：图 4. 通过迭代来计算 25 的平方根。以 25 作为 bestGuess 的初始值，该函数需要八次迭代才能计算出 25 的平方根。但是，如果在设计 bestGuess 初始值上犯下荒谬的错误，那将怎么办？ 尝试第二次，那 100 万可能是 25 的平方根吗？ 在这种明显错误的情况下会发生什么？你写的函数是否能够处理这种低级错误。直接来吧。回到测试中来，这次以一百万开始：图 5. 在计算 25 的平方根时，运用逐步求精法，以 100 万作为 bestGuess 的初始值。哇！ 以一个荒谬的数字开始，迭代次数仅增加了两倍（从八次迭代到 23 次）。增长幅度没有你直觉中预期的那么大。故事的寓意啊哈！ 当你意识到，迭代不仅能够保证解决问题，而且与你的解决方案的初始猜测值是好是坏也没有关系。 不论你最初理解得多么不正确，迭代过程以及可衡量的测试/目标，都可以使你走上正确的道路并得到解决方案。图 4 和 5 显示了陡峭而戏剧性的燃尽图。一个非常错误的开始，迭代很快就产生了一个绝对正确的解决方案。简而言之，这种神奇的方法就是敏捷 DevOps 的本质。回到一些更深层次的观察敏捷 DevOps 的实践源于人们对所生活的世界的认知。我们生活的世界存在不确定性、不完整性以及充满太多的困惑。从科学/哲学的角度来看，这些特征得到了 海森堡的不确定性原理 Heisenberg’s Uncertainty Principle （涵盖不确定性部分）， 维特根斯坦的逻辑论哲学 Wittgenstein’s Tractatus Logico-Philosophicus （歧义性部分）， 哥德尔的不完全性定理 Gödel’s incompleteness theorems （不完全性方面）以及 热力学第二定律 Second Law of Thermodynamics （无情的熵引起的混乱）的充分证明和支持。简而言之，无论你多么努力，在尝试解决任何问题时都无法获得完整的信息。因此，放下傲慢的姿态，采取更为谦虚的方法来解决问题对我们会更有帮助。谦卑会给为你带来巨大的回报，这个回报不仅是你期望的一个解决方案，还会有它的副产品。总结大自然在不停地运作，这是一个持续不断的过程。大自然没有总体规划。一切都是对先前发生的事情的回应。 反馈循环是非常紧密的，明显的进步/倒退都是逐步实现的。大自然中随处可见，任何事物的都在以一种或多种形式逐步完善。敏捷 DevOps 是工程模型逐渐成熟的一个非常有趣的结果。DevOps 基于这样的认识，即你所拥有的信息总是不完整的，因此你最好谨慎进行。获得可衡量的测试（例如，假设、可测量的期望结果），进行简单的尝试，大多数情况下可能失败，然后收集反馈，修复故障并继续测试。除了同意每个步骤都必须要有可衡量的假设/测试之外，没有其他方法。在本系列的下一篇文章中，我将仔细研究变异测试是如何提供及时反馈来推动实现结果的。via: https://opensource.com/article/19/8/mutation-testing-evolution-tdd作者：Alex Bunardzic 选题：lujun9972 译者：Morisun029 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出