蚊章汇 - 测试
type: channel
link: https://linuxcn.bwsl.wang/tag/%E6%B5%8B%E8%AF%95/
items: ZOMBIES：为什么简洁性是交付健壮软件的关键（五）
items: ZOMBIES：如何在软件开发中实现业务需求（四）
items: ZOMBIES：在软件开发中定义边界和接口（三）
items: ZOMBIES：软件开发和测试中的构建与拓展（二）
items: 用 Python 测试 API 的 3 种方式
items: 利用 BATS 测试 Bash 脚本和库
items: Python 突变测试介绍
items: CI/CD 中的自动化测试的概要知识
items: 使用 PyHamcrest 执行健壮的单元测试
items: 使用 Selenium 自动化 Web 浏览器

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…

用 Python 测试 API 的 3 种方式
type: item
date: 2022-08-19T02:08:00+08:00
description: 单元测试可能令人生畏，但是这些 Python 模块会使你的生活变得更容易。在这个教程中，你将学到如何对执行 HTTP 请求代码的进行单元测试。也就是说，你将看到用 Python 对 API 进行单元测试的艺术。单元测试是指对单个行为的测试。在测试中，一个众所周知的经验法则就是隔离那些需要外部依赖的代码。比如，当测试一段执行 HTTP 请求的代码时，建议在测试过程中，把真正的调用替换成一个假的的调用。这种情况下，每次运行测试的时候，就可以对它进行单元测试，而不需要执行一个真正的 HTTP 请求。问题就是，怎样才能隔离这些代码？这就是我希望在这篇博文中回答的问题！我不仅会向你展示如果去做，而且也会权衡不同方法之间的优点和缺点。要求：Python 3.8pytest-mockrequestsflaskresponsesVCR.py使用一个天气状况 REST API 的演示程序为了更好的解决这个问题，假设你正在创建一个天气状况的应用。这个应用使用第三方天气状况 REST API 来检索一个城市的天气信息。其中一个需求是生成一个简单的 HTML 页面，像下面这个图片：伦敦的天气，OpenWeatherMap。图片是作者自己制作的。为了获得天气的信息，必须得去某个地方找。幸运的是，通过 OpenWeatherMap 的 REST API 服务，可以获得一切需要的信息。好的，很棒，但是我该怎么用呢？通过发送一个 GET 请求到：https://api.openweathermap.org/data/2.5/weather?q={city_name}&appid={api_key}&units=metric，就可以获得你所需要的所有东西。在这个教程中，我会把城市名字设置成一个参数，并确定使用公制单位。检索数据使用 requests 模块来检索天气数据。你可以创建一个接收城市名字作为参数的函数，然后返回一个 JSON。JSON 包含温度、天气状况的描述、日出和日落时间等数据。下面的例子演示了这样一个函数：def find_weather_for(city: str) -> dict: """Queries the weather API and returns the weather data for a particular city.""" url = API.format(city_name=city, api_key=API_KEY) resp = requests.get(url) return resp.json() 这个 URL 是由两个全局变量构成：BASE_URL = "https://api.openweathermap.org/data/2.5/weather" API = BASE_URL + "?q={city_name}&appid={api_key}&units=metric" API 以这个格式返回了一个 JSON：{ "coord": { "lon": -0.13, "lat": 51.51 }, "weather": [ { "id": 800, "main": "Clear", "description": "clear sky", "icon": "01d" } ], "base": "stations", "main": { "temp": 16.53, "feels_like": 15.52, "temp_min": 15, "temp_max": 17.78, "pressure": 1023, "humidity": 72 }, "visibility": 10000, "wind": { "speed": 2.1, "deg": 40 }, "clouds": { "all": 0 }, "dt": 1600420164, "sys": { "type": 1, "id": 1414, "country": "GB", "sunrise": 1600407646, "sunset": 1600452509 }, "timezone": 3600, "id": 2643743, "name": "London", "cod": 200 当调用 resp.json() 的时候，数据是以 Python 字典的形式返回的。为了封装所有细节，可以用 dataclass 来表示它们。这个类有一个工厂方法，可以获得这个字典并且返回一个 WeatherInfo 实例。这种办法很好，因为可以保持这种表示方法的稳定。比如，如果 API 改变了 JSON 的结构，就可以在同一个地方（from_dict 方法中）修改逻辑。其他代码不会受影响。你也可以从不同的源获得信息，然后把它们都整合到 from_dict 方法中。@…

利用 BATS 测试 Bash 脚本和库
type: item
date: 2021-03-12T05:47:14+08:00
description: Bash 自动测试系统可以使 Bash 代码也通过 Java、Ruby 和 Python 开发人员所使用的同类测试过程。用 Java、Ruby 和 Python 等语言编写应用程序的软件开发人员拥有复杂的库，可以帮助他们随着时间的推移保持软件的完整性。他们可以创建测试，以在结构化环境中通过执行一系列动作来运行应用程序，以确保其软件所有的方面均按预期工作。当这些测试在持续集成（CI）系统中自动进行时，它们的功能就更加强大了，每次推送到源代码库都会触发测试，并且在测试失败时会立即通知开发人员。这种快速反馈提高了开发人员对其应用程序功能完整性的信心。 Bash 自动测试系统 Bash Automated Testing System （BATS）使编写 Bash 脚本和库的开发人员能够将 Java、Ruby、Python 和其他开发人员所使用的相同惯例应用于其 Bash 代码中。安装 BATSBATS GitHub 页面包含了安装指令。有两个 BATS 辅助库提供更强大的断言或允许覆写 BATS 使用的 Test Anything Protocol（TAP）输出格式。这些库可以安装在一个标准位置，并被所有的脚本引用。更方便的做法是，将 BATS 及其辅助库的完整版本包含在 Git 仓库中，用于要测试的每组脚本或库。这可以通过 git 子模块 系统来完成。以下命令会将 BATS 及其辅助库安装到 Git 知识库中的 test 目录中。git submodule init git submodule add https://github.com/sstephenson/bats test/libs/bats git submodule add https://github.com/ztombol/bats-assert test/libs/bats-assert git submodule add https://github.com/ztombol/bats-support test/libs/bats-support git add . git commit -m 'installed bats' 要克隆 Git 仓库并同时安装其子模块，请在 git clone 时使用 --recurse-submodules 标记。每个 BATS 测试脚本必须由 bats 可执行文件执行。如果你将 BATS 安装到源代码仓库的 test/libs 目录中，则可以使用以下命令调用测试：./test/libs/bats/bin/bats <测试脚本的路径> 或者，将以下内容添加到每个 BATS 测试脚本的开头：#!/usr/bin/env ./test/libs/bats/bin/bats load 'libs/bats-support/load' load 'libs/bats-assert/load' 并且执行命令 chmod +x <测试脚本的路径>。 这将 a、使它们可与安装在 ./test/libs/bats 中的 BATS 一同执行，并且 b、包含这些辅助库。BATS 测试脚本通常存储在 test 目录中，并以要测试的脚本命名，扩展名为 .bats。例如，一个测试 bin/build 的 BATS 脚本应称为 test/build.bats。你还可以通过向 BATS 传递正则表达式来运行一整套 BATS 测试文件，例如 ./test/lib/bats/bin/bats test/*.bats。为 BATS 覆盖率而组织库和脚本Bash 脚本和库必须以一种有效地方式将其内部工作原理暴露给 BATS 进行组织。通常，在调用或执行时库函数和运行诸多命令的 Shell 脚本不适合进行有效的 BATS 测试。例如，build.sh 是许多人都会编写的典型脚本。本质上是一大堆代码。有些人甚至可能将这堆代码放入库中的函数中。但是，在 BATS 测试中运行一大堆代码，并在单独的测试用例中覆盖它可能遇到的所有故障类型是不可能的。测试这堆代码并有足够的覆盖率的唯一方法就是把它分解成许多小的、可重用的、最重要的是可独立测试的函数。向库添加更多的函数很简单。额外的好处是其中一些函数本身可以变得出奇的有用。将库函数分解为许多较小的函数后，你可以在 BATS 测试中 援引 source 这些库，并像测试任何其他命令一样运行这些函数。Bash 脚本也必须分解为多个函数，执行脚本时，脚本的主要部分应调用这些函数。此外，还有一个非常有用的技巧，可以让你更容易地用 BATS 测试 Bash 脚本：将脚本主要部分中执行的所有代码都移到一个函数中，称为 run_main。然后，将以下内容添加到脚本的末尾：if [[ "${BASH_SOURCE[0]}" == "${0}" ]…

Python 突变测试介绍
type: item
date: 2020-11-30T07:01:53+08:00
description: 通过突变测试来修复未知的 bug。你一定对所有内容都进行了测试，也许你甚至在项目仓库中有一个徽章，标明有 100% 的测试覆盖率，但是这些测试真的帮到你了吗？你怎么知道的？开发人员很清楚单元测试的成本。测试必须要编写。有时它们无法按照预期工作：存在假告警或者抖动测试。在不更改任何代码的情况下有时成功，有时失败。通过单元测试发现的小问题很有价值，但是通常它们悄无声息的出现在开发人员的机器上，并且在提交到版本控制之前就已得到修复。但真正令人担忧的问题大多是看不见的。最糟糕的是，丢失的告警是完全不可见的：你看不到没能捕获的错误，直到出现在用户手上 —— 有时甚至连用户都看不到。有一种测试可以使不可见的错误变为可见： 突变测试 mutation testing 。变异测试通过算法修改源代码，并检查每次测试是否都有“变异体”存活。任何在单元测试中幸存下来的变异体都是问题：这意味着对代码的修改（可能会引入错误）没有被标准测试套件捕获。Python 中用于突变测试的一个框架是 mutmut。假设你需要编写代码来计算钟表中时针和分针之间的角度，直到最接近的度数，代码可能会这样写：def hours_hand(hour, minutes): base = (hour % 12 ) * (360 // 12) correction = int((minutes / 60) * (360 // 12)) return base + correction def minutes_hand(hour, minutes): return minutes * (360 // 60) def between(hour, minutes): return abs(hours_hand(hour, minutes) - minutes_hand(hour, minutes)) 首先，写一个简单的单元测试：import angle def test_twelve(): assert angle.between(12, 00) == 0 足够了吗？代码没有 if 语句，所以如果你查看覆盖率：$ coverage run `which pytest` ============================= test session starts ============================== platform linux -- Python 3.8.3, pytest-5.4.3, py-1.8.2, pluggy-0.13.1 rootdir: /home/moshez/src/mut-mut-test collected 1 item tests/test_angle.py . [100%] ============================== 1 passed in 0.01s =============================== 完美！测试通过，覆盖率为 100%，你真的是一个测试专家。但是，当你使用突变测试时，覆盖率会变成多少？$ mutmut run --paths-to-mutate angle.py <snip> Legend for output: ? Killed mutants. The goal is for everything to end up in this bucket. ⏰ Timeout. Test suite took 10 times as long as the baseline so were killed. ? Suspicious. Tests took a long time, but not long enough to be fatal. ? Survived. This means your tests needs to be expanded. ? Skipped. Skipped. <snip> ⠋ 21/21 ? 5 ⏰ 0 ? 0 ? 16 ? 0 天啊，在 21 个突变体中，有 16 个存活。只有 5 个通过了突变测试，但是，这意味着什么呢？对于每个突变测试，mutmut 会修改部分源代码，以模拟潜在的错误，修改的一个例子是将 > 比较更改为 >=，查看会发生什么。如果没有针对这个边界条件的单元测试，那么这个突变将会“存活”：这是一个没有任何测试用例能够检测到的潜在错误。是时候编写更好的单元测试了。很容易检查使用 results 所做的更改：$ mutmut results <snip> Survived ? (16) ---- angle.py (16) ---- 4-7, 9-14, 16-21 $ mutmut apply 4 $ git diff diff …

CI/CD 中的自动化测试的概要知识
type: item
date: 2020-07-28T05:01:32+08:00
description: 持续集成和持续交付是由测试驱动的。以下是如何做到的。“如果一切似乎都在控制之中，那只是你走的不够快而已。” —Mario Andretti测试自动化是指在软件开发过程中尽可能早、尽可能快地持续关注检测缺陷、错误和 bug。这是通过使用那些追求质量为最高价值的工具完成的，它们旨在确保质量，而不仅仅是追求质量。持续集成/持续交付（CI/CD）解决方案（也称为 DevOps 管道）最引人注目的功能之一是可以更频繁地进行测试，而又不会给开发人员或操作人员增加更多的手动工作。让我们谈谈为什么这很重要。为什么要在 CI/CD 中实现自动化测试？敏捷团队要更快的迭代，以更高的速度交付软件和客户满意度，而这些压力可能会危及质量。全球竞争制造了对缺陷的低容忍度，同时也增加了敏捷团队的压力，要求软件交付的迭代更快。减轻这种压力的行业解决方案是什么？是 DevOps。DevOps 是一个大概念，有很多定义，但是对 DevOps 成功至关重要的一项技术是 CI/CD。通过软件开发流程设计一个连续的改进循环，可以为测试带来新的机会。这对测试人员意味着什么？对于测试人员，这通常意味着他们必须：更早且更频繁地进行测试（使用自动化）持续测试“真实世界”的工作流（自动和手动）更具体地说，任何形式的测试，无论是由编写代码的开发人员运行还是由质量保证工程师团队设计，其作用都是利用 CI/CD 基础架构在快速推进的同时提高质量。测试人员还需要做什么？具体点说，测试人员负责：测试新的和现有的软件应用根据系统要求评估软件来验证和确认功能利用自动化测试工具来开发和维护可重复使用的自动化测试与 scrum 团队的所有成员合作，了解正在开发的功能以及实施的技术设计，以设计和开发准确、高质量的自动化测试分析记录在案的用户需求，并针对中等到高度复杂的软件或 IT 系统制定或协助设计测试计划开发自动化测试，并与功能团队一起审查和评估测试方案与技术团队合作，确定在开发环境中自动化测试的正确方法与团队合作，通过自动化测试来了解和解决软件问题，并回应有关修改或增强的建议参与需求梳理、估算和其他敏捷 scrum 仪式协助制定标准和流程，以支持测试活动和材料（例如脚本、配置、程序、工具、计划和结果）测试是一项艰巨的工作，但这是有效构建软件的重要组成部分。哪些持续测试很重要？你可以使用多种测试。不同的类型并不是学科之间的牢固界限。相反，它们是表示如何测试的不同方式。比较测试类型不太重要，更重要的是对每一种测试类型都要有覆盖率。功能测试： 确保软件具有其要求的功能单元测试： 独立测试软件的较小单元/组件以检查其功能负载测试： 测试软件在重负载或使用期间的性能压力测试： 确定软件承受压力（最大负载）时的断点集成测试： 测试组合或集成的一组组件的输出回归测试： 当修改任意组件（无论多么小），测试整个应用的功能总结任何包含持续测试的软件开发过程都将朝着建立关键反馈环路的方向发展，以实现快速和构建有效的软件。最重要的是，该实践将质量内置到 CI/CD 管道中，并意味着了解在软件开发生命周期中提高速度同时减少风险和浪费之间的联系。via: https://opensource.com/article/20/7/automation-testing-cicd作者：Taz Brown 选题：lujun9972 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出

使用 PyHamcrest 执行健壮的单元测试
type: item
date: 2019-01-29T08:33:20+08:00
description: 使用此框架编写断言，提高开发测试的准确性。在测试金字塔的底部是单元测试。单元测试每次只测试一个代码单元，通常是一个函数或方法。通常，设计单个单元测试是为了测试通过一个函数或特定分支的特定执行流程，这使得将失败的单元测试和导致失败的 bug 对应起来变得容易。理想情况下，单元测试很少使用或不使用外部资源，从而隔离它们并使它们更快。单元测试套件通过在开发过程的早期发现问题来帮助维护高质量的产品。有效的单元测试可以在代码离开开发人员机器之前捕获 bug，或者至少可以在特定分支上的持续集成环境中捕获 bug。这标志着好的和坏的单元测试之间的区别：好的测试通过尽早捕获 bug 并使测试更快来提高开发人员的生产力。坏的测试降低了开发人员的工作效率。当测试附带的特性时，生产率通常会降低。当代码更改时测试会失败，即使它仍然是正确的。发生这种情况是因为输出的不同，但在某种程度上是因为它不是 函数契约 function’s contract 的一部分。因此，一个好的单元测试可以帮助执行函数所提交的契约。如果单元测试中断，那意味着该契约被违反了，应该（通过更改文档和测试）明确修改，或者（通过修复代码并保持测试不变）来修复。虽然将测试限制为只执行公共契约是一项需要学习的复杂技能，但有一些工具可以提供帮助。其中一个工具是 Hamcrest，这是一个用于编写断言的框架。最初是为基于 Java 的单元测试而发明的，但它现在支持多种语言，包括 Python。Hamcrest 旨在使测试断言更容易编写和更精确。def add(a, b): return a + b from hamcrest import assert_that, equal_to def test_add(): assert_that(add(2, 2), equal_to(4)) 这是一个用于简单函数的断言。如果我们想要断言更复杂的函数怎么办？def test_set_removal(): my_set = {1, 2, 3, 4} my_set.remove(3) assert_that(my_set, contains_inanyorder([1, 2, 4])) assert_that(my_set, is_not(has_item(3)))注意，我们可以简单地断言其结果是任何顺序的 1、2 和 4，因为集合不保证顺序。我们也可以很容易用 is_not 来否定断言。这有助于我们编写精确的断言，使我们能够把自己限制在执行函数的公共契约方面。然而，有时候，内置的功能都不是我们真正需要的。在这些情况下，Hamcrest 允许我们编写自己的 匹配器 matchers 。想象一下以下功能：def scale_one(a, b): scale = random.randint(0, 5) pick = random.choice([a,b]) return scale * pick我们可以自信地断言其结果均匀地分配到至少一个输入。匹配器继承自 hamcrest.core.base_matcher.BaseMatcher，重写两个方法：class DivisibleBy(hamcrest.core.base_matcher.BaseMatcher): def __init__(self, factor): self.factor = factor def _matches(self, item): return (item % self.factor) == 0 def describe_to(self, description): description.append_text('number divisible by') description.append_text(repr(self.factor))编写高质量的 describe_to 方法很重要，因为这是测试失败时显示的消息的一部分。def divisible_by(num): return DivisibleBy(num)按照惯例，我们将匹配器包装在一个函数中。有时这给了我们进一步处理输入的机会，但在这种情况下，我们不需要进一步处理。def test_scale(): result = scale_one(3, 7) assert_that(result, any_of(divisible_by(3), divisible_by(7)))请注意，我们将 divisible_by 匹配器与内置的 any_of 匹配器结合起来，以确保我们只测试函数提交的内容。在编辑这篇文章时，我听到一个传言，取 “Hamcrest” 这个名字是因为它是 “matches” 字母组成的字谜。嗯…>>> assert_that("matches", cont…

使用 Selenium 自动化 Web 浏览器
type: item
date: 2018-12-03T06:11:41+08:00
description: Selenium 是浏览器自动化的绝佳工具。使用 Selenium IDE，你可以录制命令序列（如单击、拖动和输入），验证结果并最终存储此自动化测试供日后使用。这非常适合在浏览器中进行活跃开发。但是当你想要将这些测试与 CI/CD 流集成时，是时候使用 Selenium WebDriver 了。WebDriver 公开了一个绑定了许多编程语言的 API，它允许你将浏览器测试与其他测试集成。这篇文章向你展示了如何在容器中运行 WebDriver 并将其与 Python 程序一起使用。使用 Podman 运行 SeleniumPodman 是下面例子的容器运行时。有关如何开始使用 Podman 的信息，请参见此前文章。此例使用了 Selenium 的独立容器，其中包含 WebDriver 服务器和浏览器本身。要在后台启动服务器容器，请运行以下命令：$ podman run -d --network host --privileged --name server docker.io/selenium/standalone-firefox当你使用特权标志和主机网络运行容器时，你可以稍后从在 Python 中连接到此容器。你不需要使用 sudo。在 Python 中使用 Selenium现在你可以提供一个使用此服务器的简单程序。这个程序很小，但应该会让你知道可以做什么：from selenium.webdriver.common.desired_capabilities import DesiredCapabilities server ="http://127.0.0.1:4444/wd/hub" driver = webdriver.Remote(command_executor=server, desired_capabilities=DesiredCapabilities.FIREFOX) print("Loading page...") driver.get("https://fedoramagazine.org/") print("Loaded") assert "Fedora" in driver.title driver.quit() print("Done.")首先，程序连接到你已经启动的容器。然后它加载 Fedora Magazine 网页并判断 “Fedora” 是页面标题的一部分。最后，它退出会话。需要 Python 绑定才能运行此程序。既然你已经在使用容器了，为什么不在容器中这样做呢？将以下内容保存到 Dockerfile 中：FROM fedora:29 RUN dnf -y install python3 RUN pip3 install selenium然后使用 Podman 在与 Dockerfile 相同的文件夹中构建容器镜像：$ podman build -t selenium-python .要在容器中运行程序，在运行容器时将包含 Python 代码的文件作为卷挂载：$ podman run -t --rm --network host \ -v $(pwd)/browser-test.py:/browser-test.py:z \ selenium-python python3 browser-test.py输出看上去像这样：Loading page... Loaded Done.接下来做什么上面的示例程序是最小的，也许没那么有用。但这仅仅是最表面的东西！查看 Selenium 和 Python 绑定 的文档。在那里，你将找到有关如何在页面中查找元素、处理弹出窗口或填写表单的示例。拖放也是可能的，当然还有等待事件。在实现一些不错的测试后，你可能希望将它们包含在 CI/CD 流程中。幸运的是，这是相当直接的，因为一切都是容器化的。你可能也有兴趣设置 grid 来并行运行测试。这不仅有助于加快速度，还允许你同时测试多个不同的浏览器。清理当你容器使用完后，可以使用以下命令停止并删除独立容器：$ podman stop server $ podman rm server如果你还想释放磁盘空间，请运行以下命令删除镜像：$ podman rmi docker.io/selenium/standalone-firefox $ podman rmi selenium-python fedora:29总结在本篇中，你已经看到使用容器技术开始使用 Selenium 是多么容易。它允许你自动化与网站的交互，以及测试交互。Podman 允许你在没有超级用户权限或 Docker 守护程序的情况下运行所需的容器。最后，Python 绑定允许你使用普通的 Python 代码与浏览器进行交互。via: https://fedo…