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


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

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

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

看起来很简单。假设您有一个小函数可以做一件事。假设它被称为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模拟代码
  • 确保您的测试可能会失败
  • 将不确定性排除在测试之外

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

最后: 可以在公众号:伤心的辣条 ! 免费领取一份216页软件测试工程师面试宝典文档资料。以及相对应的视频学习教程免费分享!,其中包括了有基础知识、Linux必备、Shell、互联网程序原理、Mysql数据库、抓包工具专题、接口测试工具、测试进阶-Python编程、Web自动化测试、APP自动化测试、接口自动化测试、测试高级持续集成、测试架构开发测试框架、性能测试、安全测试等。

如果我的博客对你有帮助、如果你喜欢我的博客内容,请 “点赞” “评论” “收藏” 一键三连哦!喜欢软件测试的小伙伴们,可以加入我们的测试技术交流扣扣群:914172719(里面有各种软件测试资源和技术讨论)


好文推荐

转行面试,跳槽面试,软件测试人员都必须知道的这几种面试技巧!

面试经:一线城市搬砖!又面软件测试岗,5000就知足了…

面试官:工作三年,还来面初级测试?恐怕你的软件测试工程师的头衔要加双引号…

什么样的人适合从事软件测试工作?

那个准点下班的人,比我先升职了…

测试岗反复跳槽,跳着跳着就跳没了…

如何避免单元测试陷阱?相关推荐

  1. 事务策略: 了解事务陷阱--转

    在 Java 平台中实现事务时要注意的常见错误 在应用程序中使用事务常常是为了维护高度的数据完整性和一致性.如果不关心数据的质量,就不必使用事务.毕竟,Java 平台中的事务支持会降低性能,引发锁定问 ...

  2. 测试驱动开发 测试前移_测试驱动陷阱,第2部分

    测试驱动开发 测试前移 单元测试中单元的故事 在本文的上半部分 ,您可能会看到一些不好但很受欢迎的测试示例. 但是我不是一个专业的批评家(也被称为"巨魔"或"仇恨者&qu ...

  3. 测试驱动陷阱,第2部分

    单元测试中单元的故事 在本文的上半部分 ,您可能会看到一些不好但很流行的测试示例. 但是我不是一个专业评论家(也被称为"巨魔"或"仇恨者"),没有任何建设性的话 ...

  4. python单元测试mock_Mock 在 Python 单元测试中的使用

    本文讲述的是 Python 中 Mock 的使用. 如何执行单元测试而不用考验你的耐心 很多时候,我们编写的软件会直接与那些被标记为"垃圾"的服务交互.用外行人的话说:服务对我们的 ...

  5. iOS开发中的单元测试(三)——URLManager中的测试用例解析

    本文转载至 http://www.cocoachina.com/cms/plus/view.php?aid=8088   此前,我们在<iOS开发中的单元测试(一)&(二)>中介绍 ...

  6. Android单元测试(七):Robolectric,在JVM上调用安卓的类

    2019独角兽企业重金招聘Python工程师标准>>> 今天讲讲Android上做单元测试的最后一个难点,那就是在JVM上无法调用安卓相关的类,不然的话,会报类似于下的错误: jav ...

  7. 实施ASP.NET Core应用程序的常见陷阱

    Special thanks to Matthew Wilkin for kindly helping to peer review this article. 特别感谢Matthew Wilkin慷 ...

  8. JAVA拾遗 — JMH与8个代码陷阱

    作者:kiritomoe 来源:Kirito的技术分享 前言 JMH (http://openjdk.java.net/projects/code-tools/jmh/) 是 Java Microbe ...

  9. 单元测试 chapter3

    #chapter3 本章涵盖单元测试的结构单元测试命名最佳实践使用参数化测试处理流利的断言 在第1部分的其余章节中,我将向您介绍一些基本主题. 我将介绍典型的单元测试的结构,该结构通常由安排,行动和声 ...

最新文章

  1. 从一次故障聊聊前端 UI 自动化测试
  2. avrorecord.java,失败,但发生异常java.io.IOException:org.apache.avro.AvroTypeException:发现的很长,期望在配置单元中实现联合...
  3. codeforces Gym 100338E Numbers (贪心,实现)
  4. XMind2020的一些使用小技巧
  5. Py之pyecharts:python包之数据可视化包pyecharts简介、安装、使用方法之详细攻略
  6. DEV ImageComboxEdit 使用
  7. SAP ABAP和Hybris Commerce的Sample数据
  8. 听歌也能倍速了!网易云音乐PM怎么想的?
  9. 收获,不止SQL优化——抓住SQL的本质--第八章
  10. R和RStudio下载安装详细步骤
  11. 【实用工具】eclipse mac安装
  12. cocoapods导入第三方库
  13. Origin 2019b 64Bit 软件绘制出图的坐标刻度老是消失怎么解决
  14. JavaWeb项目间隔刷新出现412
  15. excel做ns流程图_如何制作传统流程图和NS流程图教程详解.ppt
  16. 服务器托管的费用介绍
  17. x86代表电脑的操作系统是32位 和 x64代表电脑的操作系统是64位
  18. 2021年1~12月语音合成和语音识别论文月报
  19. 开机直接进bios,重启机器能正常进入系统,是什么问题?
  20. 动网论坛缓存技术研究

热门文章

  1. 不抛出异常的swap
  2. iis绑定php程序应用池设定,什么是IIS应用程序池以及应用程序池详解
  3. python连接传感器_树莓派4B之光敏传感器模块(python3)
  4. 工作展望简短_元旦祝福语大全简短
  5. 计算机图形直线分析,基本图形分析法:等腰三角形(一)
  6. VC编程操作Word2010生成表格
  7. 基于机器视觉的磁头飞机载划痕检测
  8. Java必备技能:IDEA一定要懂的32条快捷键
  9. 前端清单之Vue.js篇
  10. 关于Cocos2d-x发布游戏的时候遇到的问题和解决