IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 开发测试 -> 单元测试最佳实践|如何避免常见陷阱? -> 正文阅读

[开发测试]单元测试最佳实践|如何避免常见陷阱?

单元测试的目的是为了随着时间的变化,系统能够按预期工作。一来系统质量得到了保证,开发人员能够提前发现和解决问题,不用身陷bug的泥潭无法自拔;二来开发人员有更多的时间和精力去完善自己技术、提升自己的生活质量,从而形成一个良性循环。

fbc3ba2f4eaf31b24725dbb496968ec5.png

我写了很多测试,也读了很多。他们中的大多数帮助我及早发现错误,提供代码文档并帮助回归测试。但我也发现一些单元测试没有做到这一点。相反,它们要么非常复杂,以至于无法弄清楚它们在测试什么,要么会随机失败,要么根本不会失败。

本文介绍了导致单元测试无效的五个陷阱,以及如何修复它们。

为每个函数编写一个单元测试

看起来很简单。假设您有一个小函数可以做一件事。假设它被称为calculate_average。它是一个小单元,它是单元测试最佳实践希望您测试的单元。所以你为它写了一个测试,test_calculate_average.

这有什么问题?它测试单个代码单元,但它应该测试该单元的单个行为。通常这也被表述为在测试中只有一个断言。一个更好的测试将是test_calculate_average_return_0_for_empty_list. 一旦您拥有了其中的几个,他们就会免费为您提供详细的文档。

它还改变了您对如何编写测试的思维方式。您必须考虑您期望从函数中获得的不同行为。在不知不觉中,场景越来越多,因为您正在考虑边缘情况,甚至为它们编写测试,所以编写单元测试的收益也逐渐降低。

为每个功能单元编写一个单元测试,而不是代码单元。

测试的重点应该是外部行为,如果我们过渡关注内部行为,当我们对实现逻辑进行了修改,那么原本的单元测试也就无法使用了,也起不到对代码重构保驾护航的作用了,违背了我们写单元测试的初衷,当然如果有一块内部逻辑,非常复杂,你也可以自己进行全覆盖测试,但一般情况下没有必要为了测试而测试。

只为代码覆盖率而编写测试

跟踪测试覆盖率通常是一个好主意。如今,许多测试框架都支持这一点,并且像codecov这样的平台可以很容易地随着时间的推移对其进行跟踪。那么,为什么沉迷于它不是一个好的想法呢?

代码覆盖率只是一种测量工具。100% 的代码覆盖率并不意味着你已经覆盖了所有的边缘情况,它只是意味着所有的代码路径都被执行了。这是一个覆盖率 100% 的快速反例,但让我们探讨当您传入一个空列表时会发生什么?

def?average(elements:?List[int]):
??return?sum(elements)?/?len(elements)

def?test_average_returns_average_of_list:
??result?=?average([1,3,5,7])
??assert?result?==?4

代码覆盖率的根本问题是它只衡量覆盖了多少行程序。但所有程序都是状态机;要获得完整覆盖,您必须覆盖所有状态,但这是不可行的。

追求完整的,或者至少是非常高的覆盖率也会导致大量的测试,但并不是所有的测试都那么有用。对于胶水代码尤其如此。我见过模拟 Web 框架 (flask) 一半的测试,只是为了测试为端点注册函数是否有效。这是测试一小部分功能的大量工作。如果你弄错了,那就很明显了。一旦你做对了,它在未来不太可能改变。

我没有努力覆盖每一行代码,而是推荐 Martin Fowler 的建议。将测试重点放在有风险的代码上。那是您自己编写的代码,而不是可能会被重构的框架。然而,知道什么是有风险的很困难,因为它需要经验。

您应该将 [您的测试工作] 集中在风险点上。— Martin Fowler,重构

特别是某个代码逻辑导致的线上bug,或者其它同学发现的问题,都可以编写成测试用例,防止此类错误的再次出现。

严重依赖Mock

使用打桩模拟和存根对于单元测试是必不可少的。大多数情况下,您的被测代码与其他模块交互,并且在测试期间,您希望控制它们的行为。这可能导致你过度打桩。

当您必须编写 50 或 100 行模拟来测试单个函数时,那么您在测试什么?您是在测试您的函数,还是在测试您为测试该函数而编写的模拟?

许多Mock模拟也是危险信号。当您需要多个非常复杂的模拟来测试单个函数时,这个函数很可能复杂度过高。因此,您可能希望将其重构为几个功能较少且可以单独测试的函数。

我见过一些非常复杂的模拟。这是一个例子的再现:

#?custom_middleware.py?####################################
class?CustomHeaderMiddleware(BaseHTTPMiddleware):
????async?def?dispatch(self,?request,?call_next):????
????????response?=?await?call_next(request)
????????response.headers["CustomField"]?=?"bla"
????????return?response

#?test_custom_middleware.py?###############################
async?def?endpoint_for_test(_):
????return?PlainTextResponse("Test")

middleware?=?[Middleware(CustomHeaderMiddleware)]
routes?=?[Route("/test",?endpoint=endpoint_for_test)]
app?=?Starlette(routes=routes,?middleware=middleware)

@pytest.mark.asyncio
async?def?test_middleware_sets_field():
????client?=?TestClient(app)
????response?=?client.get("/test")
????assert?response.headers["CustomField"]?==?"bla"

这个时候,你不要想办法进行Mock模拟,而是考虑如何进行重构?让其变得更简单,更容易测试。

我们通常通过单元测试去保证代码质量,那么单元测试代码本身的质量又如何保证呢?所以我们的单元测试要写的尽可能简单。

对于对数据一致性要求不高的系统,甚至可以直接对着接口进行测试,这样省去了编写Mock的复杂度。

编写永不失败的单元测试

正常情况下,回归是进行单元测试的原因之一。您编写代码,编写通过的测试并获得收益。万一有人破坏了您代码的功能,单元测试将能够发现问题。然而,另外一种情况,您的测试可能永远不会失败并且您会错过回归。

但是,您如何以永不失败的测试结束呢?下面是一个例子:

def?get_film(id:?str):
????data?=?{"query":?QUERY,?"variables":?json.dumps({"id":?id})}
????response?=?requests.post(URL,?data=data)
????return?response.json()["data"]["film"]

def?test_get_film_returns_successfully():
????mock_response?=?{
????????"data":?{
????????????"film":?{
????????????"title":?"a?New?Test",
????????????"id":?"testId",
????????????"episodeID":?4
????????????}
????????}
????}
????with?requests_mock.Mocker()?as?mock:
????????mock.post(URL,?json=mock_response)
????????result?=?get_film("foo")
????????assert?result?==?{
????????????"title":?"a?New?Test",
????????????"id":?"testId",
????????????"episodeID":?4
????????}

现在问问自己:哪些更改会导致此测试失败?最明显的一个是改变Mock模拟响应。但这不算数,您没有更改被测代码。更糟糕的是,我忘记了传递json.dumps参数. 这个错误不会被测试发现。另外有的同学为了保证测试覆盖率,甚至不写断言,直接打印输出,这样的话,可能永远不会出错。

这种问题被称为误报,看似无懈可击的测试用例,其实没什么用处,为了防止这种情况,请考虑是什么导致您的测试失败。更好的是,从失败的测试开始,然后编写代码直到它通过。在不知不觉中,您正在进行测试驱动开发。

使用单元测试保证非确定性行为的正确性

这是一个众所周知的谬论。如果您的测试或被测代码以不确定的方式运行,您将对测试失去信心。每次失败时,你都会问:我的测试失败了,还是会通过重新运行?重新修改运行都会给你的测试用例带来修改的麻烦,你甚至想要放弃单元测试用例。

对于测试来说,不确定性的缺点是显而易见的,那么是什么导致了这种情况呢?

您是否在测试中使用当前时间或日期?如果是,则您的测试每天都在使用不同的数据运行。一旦您从事该行业的时间足够长,您就会遇到这些类型的测试。它们可能仅在该月的最后一天失败,或者仅在午夜之前开始并在之后完成。幸运的是,有一个简单的解决方案:控制时间的流动。例如,Python 具有用于此的freeze-gun模块。

您是否使用随机性来生成示例数据?有一个名为faker的 Python 库,它可以轻松生成真实的数据,如姓名、地址或电话号码。它非常适合填充演示环境或冒烟测试。对于单元测试不是那么有用,通常而言,使用硬编码的单元测试用例最可靠。

如果系统中存在不确定性,那么应该保证固定的逻辑不会出错,对于不确定性的边缘情况应该通过其它方式保证,比如开发、测试人员、寻找更稳定的类库等。

总结

这就是阻止您编写有效单元测试的五个陷阱。既然您了解它们,您可以通过执行以下操作来避免它们:

  • 为功能的每个部分而不是每个函数编写测试

  • 不痴迷于代码覆盖率,而是专注于测试有风险的代码

  • 最小化Mock模拟代码

  • 确保您的测试可能会失败

  • 将不确定性排除在测试之外

这将使您的系统更加稳定,另外经过良好测试的软件让您可以自信地进行更改和快速部署。

引用

https://github.com/google/googletest

https://betterprogramming.pub/advanced-unit-tests-5-pitfalls-and-how-to-avoid-them-eb6e04ec9654

https://developer.ibm.com/articles/au-googletestingframework/

https://www.froglogic.com/blog/code-coverage-of-unit-tests-written-with-google-test/

推荐

A Big Picture of Kubernetes

Kubernetes入门培训(内含PPT)


原创不易,随手关注或者”在看“,诚挚感谢!

  开发测试 最新文章
pytest系列——allure之生成测试报告(Wind
某大厂软件测试岗一面笔试题+二面问答题面试
iperf 学习笔记
关于Python中使用selenium八大定位方法
【软件测试】为什么提升不了?8年测试总结再
软件测试复习
PHP笔记-Smarty模板引擎的使用
C++Test使用入门
【Java】单元测试
Net core 3.x 获取客户端地址
上一篇文章      下一篇文章      查看所有文章
加:2021-10-11 17:48:24  更:2021-10-11 17:49:32 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/18 2:46:47-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码