如何编写有效的单元测试

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前, 星球 内第2个项目《仿小红书(微服务架构)》正在更新中。第1个项目:全栈前后端分离博客项目已经完结,演示地址:http://116.62.199.48/。采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 255 小节,累计 39w+ 字,讲解图:1716 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 1300+ 小伙伴加入,欢迎点击围观

单元测试与集成测试的主要区别在于您要测试的内容。在集成测试中,您测试整个系统放在一起时是否按预期运行,而在单元测试中,您的目标只是让重构充满信心。理想情况下,当您重构某些东西并且它被破坏时,至少有一个单元测试失败。但是当你重构某些东西并且它工作时,单元测试通过。

集成测试自然是 高杠杆 ;您通常可以用不多的代码测试大量的功能。但它们通常运行速度较慢,测试的边缘情况较少,并且往往无法让您很好地了解它们失败时损坏了什么。为了确保您的单元测试是免费的,您需要确保它们很快,测试许多边缘情况,并且只测试一件事,以便您在失败时知道什么地方坏了。

不该做什么

编写大量单元测试实际上并没有增加多少价值是很常见的。如果您有许多单元测试隐式测试同一件事,那么它们将同时失败。例如,你正在测试你所有的 Flask 视图,其中许多视图都有一个装饰器来测试用户是否登录。如果你破坏了那个装饰器,许多测试将失败。理想情况下,您希望在一组测试中测试装饰器本身,然后让其余测试模拟它。

在后一种情况下,当您重构登录的工作方式时会发生什么?希望您只需要更新少数测试。经历模拟事物的过程并且一次只测试一个单元也将提高代码本身的质量。您将看到如何更好地设计您的组件以实现关注点分离,以便它们可以单独测试。

一些常见的反模式:

  • 一个代码单元的单元测试允许该代码实际调用它的所有依赖项
  • 单元测试在开始之前加载大型数据库
  • 单元测试很慢,所以你讨厌添加更多并运行它们
  • 在一次测试中对许多事物进行断言,或者对整个嵌套的 JSON 对象进行断言,这是一回事
    • 想想如果你改变了那个 JSON 模式会发生什么
  • 忘记测试代码逻辑中的分支
  • 测试很冗长

做什么,而不是

  • 模拟代码库的其他部分
  • 尽可能模拟数据库访问
    • 或者,确保测试中的数据库访问在测试之间可以快速设置和重置
    • 您可以为此利用数据库事务
  • 设计一种机制来按需运行一个测试或一小组测试
  • 代码中的每个逻辑分支都有自己的单元测试
  • 测试采用与任何其他代码相同的 DRY 原则

示例:Flask 应用程序

这是一个带有虚构 ORM 层的示例 Flask 应用程序。

 from flask import Flask from flask.json import jsonify app = Flask(__name__) @app.route('/user/<int:user_id'>/) def get_user(user_id): if not request.user: return jsonify({'error': 'You are not logged in'}) try: user = User.objects.get(id=user_id) except NotFoundError as e: return jsonify({'error': e}) return jsonify(dict(id=user.id, name=user.name, email=user.email))

您希望为此代码编写哪些测试,它们将如何与您的其余代码隔离开来?

  • 测试如果你做一个 GET /user/1 你的视图被调用时 user_id 为 1。
    • 不需要 每次都测试 @app.route ,但你确实需要测试你定制的部分(URL)是否正常工作。
    • 测试当您使用非整数调用此路由时会发生什么。
    • 只需验证是否使用正确的参数调用 get_user ,不要在此测试中执行正文。如果这样做,那里的破损将无法通过此测试以及后续测试。
    • 例如,如果你拼错了 /user ,或者忘记指定 user_id 是一个 int 怎么办。
  • 如果未定义 request.user 测试返回的内容。那是你代码中的一个分支。
    • 如果你把它变成一个装饰器,你只想验证装饰器是否执行,但你可以将逻辑测试推迟到对该组件的测试。这是通过测试改进组件设计的示例。
  • 测试如果找不到 user_id 会发生什么。
  • 测试调用成功的返回值。
    • 同样,通过单独测试的 User.to_json() 方法可以做得更好。
    • 在这种情况下,您只需断言返回值等于 User.to_json() ,而不是实际的 JSON 是什么。

对于其中一些测试用例,您需要先在数据库中为该 user_id 创建一条记录。你的测试框架应该给你一个方便的地方来做这件事。同样,最好的方法是编写代码来创建这组测试所需的记录,而不是针对完整的数据库备份运行测试。后者可能很难维护,而且通常速度较慢。


我目前在 NerdWallet 工作,这是一家位于旧金山的初创公司,致力于让生活中的所有财务决策变得清晰。我们正在 疯狂招聘 。在 Twitter 上联系我,我很想谈谈。