谈谈 unit test?

2021-01-28 10:11:36 +08:00
 sockpuppet9527

书接上文, 谈谈 code review: /t/693941

  • 优点:保证工程质量。API 级别的 unit test 。
  • 缺点:过量的 UT 只会导致 API 不好改动,浪费更多时间前期开发+后期维护

最近我的波兰“好兄弟”脑壳一抽,提出要加 UT,早不写,晚不写,非得 API 全都弄完了再写。本来就是谁写代码,谁就应该加 UT 的事情,结果还得先让我提一个 UT plan 上去。

然后我就改了七八版本吧。他有如下要求

写 UT 从来都是好事情,波兰“好东西”就是脑壳不转弯的,现在每天下午快下班了在那吵架,搞得大家都很烦,这个 UT plan 提了几周还在改,我真是佛了。

4051 次点击
所在节点    程序员
38 条回复
xylophone21
2021-01-28 13:57:32 +08:00
@sockpuppet9527

weak symbols,这个我理解应该是指把 B.o 里的方法制定为 weak ?但这样 Mock 也只能有一个实现,也就是说对应用例 1,需要 b_foo(1)返回 0,而用例 2 需要 b_foo(1)返回 1 还是很难实现?

GMock 大概放狗找了一下。
从 Google Mock 的原理里可以看到,Google Mock 只能对类的虚函数做 mock,并且要求被测代码不能够直接创建该类的对象,而是由外界传进去的。
所以这个可能得在开发阶段就定下规则,比如虚函数,不能直接创建对象等


@feather12315
这个办法倒是很有意思,有成熟的框架吗?
sockpuppet9527
2021-01-28 14:04:14 +08:00
@xylophone21 #21

1. weak symbols 是的,正如你所说,但是默认函数都是 weak symbols 的
2. GMock 我只用来过 mock global 的方法,mock class 我没有试过。
yazoox
2021-01-28 15:03:01 +08:00
c/c++的测试写起来,那是真的痛苦!尤其是给以前的 legacy code 写…
namelosw
2021-01-28 15:17:46 +08:00
写测试是门艺术. 不写测试不好, 大部分项目写不好效果更差. 我觉得重要的就几个方面, 但是更多的需要实践和思考, 特别是在团队中的实践(因为很多写测试在所有人脑袋里的含义都不一样, 很多时候即便测试策略很好, 最终执行也会很差), 你可以结合你的经验思考一下:

一: 测试要和代码一起写. 好的测试都需要适合测试的代码配合(并发, 沙盒, 替换, 生成测试等等). TDD 虽然不是万能的, 但是很容易保证代码是可测的, 即使不 TDD 也要在开发过程中立刻写测试. 功能要一点一点加, 加一点功能写一点测试. 全写完了写测试就会导致各种问题, 因为写代码的时候根本没考虑测试.


二: 粒度相关

测试艺术就在粒度控制上. 粒度大能 cover 的业务多, 但是更慢更不稳定. 粒度小容易写, 跑得也快, 但是不能反应业务, 而且导致代码僵化没法重构. 表面上看起来怎么 trade off 都一样, 但是实际可以有很多 sweet spot.

粒度控制的核心目标就是尽量同时提高对业务的 coverage 且不明显牺牲速度:

提高对业务 coverage 就是增大粒度, 意义是你的测试是对着业务需求写的 - 很多 UT 是对着代码写, 而不是对着需求写, 这样改代码的时候就难免删测试重写, 而对着需求写的好处是 1) 业务不改的时候你也可以重构代码, 不需要改测试, 而且测试还能继续 cover, 这样你就知道重构的时候你没改错 2) 很容易理解和阅读, 标题本身都可以当文档了.

不明显牺牲速度, 即像上面说的提高粒度一般都会牺牲速度, 比如完全端对端就要把数据库之类的全连起来, 特别难搭建, 而且运行特别慢, 还没办法每个测试都清理, 非常不稳定. 但是很多通用组件其实不需要测试, 所以可以让代码 mock 掉这些. 典型的比如你测试一个 web server, 很多技术栈测试的时候会真的把 server 跑起来监听 socket, 那其实就非常浪费, 拖慢了速度还浪费了端口号. 正确的做法是, web server 一般就是经过 middlewares 然后打到 action 上, 那么理想的测试是只把 middlewares 和 actions 跑起来, 跳过真正的 socket 监听(因为真没啥好测的), 这样既能测所有逻辑, 也像所有内存代码一样快(因为 middlewares 和 actions 就是些普通函数).

除了 socket 之外, 最吃性能的就是数据库. 不过数据库比较 tricky… 如果是 Redis 这种其实非常容易 mock. 但是如果是 SQL 这种就很难 mock 了, 如果 mock 基本上等于重写一个 SQL, 一般有几个选项: 1) 硬着头皮 mock repository 或者 DAO, 不推荐, 很多存储和关联的逻辑都被 mock 掉了, 很容易写出错误的测试 2) 用真数据库, 每个测试之后删表, 但是并行没了 3) 用 SQLite 之类的, 但是必须得注意和真数据库的区别, 有时候要躲着某些功能. 或者像.Net Entity framework 之类的提供专用的的 SQL 内存实现就很爽. 4) 给某些数据库做沙箱机制, 比如 Elixir 的 Ecto 用 Postgres 但是速度非常快, 可以参考一下源码.

为什么速度这么重要呢? 因为测试跑一天也跑不完, 就没人跑了. 只在 CI 上跑的测试只能是半个测试, 对开发本身没有益处. 测试一个目的是验收, 另外一个目的是开发过程内协助开发(比如每改一段代码你就能立刻知道刚才写的对不对, 不对可以立刻 debug). 如果测试不能在开发过程里给 dev 帮助, dev 就会不想写测试.

总的目标就是以上, 我们内部把这个叫做写“集成风格的单元测试”. 效果就是测试数量相对少, 不用改, 跑得也不慢还可以并发.


三: 分层相关.

很多 UT 会分很多层, 比如 model 测试, service 测试, controller 测试, integration 测试等等. 很多专业的团队会提测试金字塔, 总得来说就是大量细粒度, 中量中粒度, 少量大粒度, 很多金字塔甚至会有六七层. 其实分太多层效果并不好 - 会出现很多你说的已经测过一遍, 结果又测一遍的情况. 但是反过来如果不测两边又不能保证合起来是对的.

而且分很多层之后, 同样的测试可能要写四五遍... 这样工作量太大, 一定会有人就会跳过某些层写测试, 这样每个人看代码库都不知道应该写哪些层了. 所以我的建议是一两层左右, 最多不超过三层. 这个要在组里面讨论好, 大家就定死一定写, 或者一定不写.

PS: 有时候测试有驱动设计的效果, 比如所有人逻辑都写在 controller 里导致很多设计问题, 想把逻辑在 model 里面表达出来, 那么可以定死所有人一定要写 model 测试, 这样逻辑就会慢慢自动回到 model 层里. 但是注意像上面说的弄太多层.

UI 或者表现层相关的最后一步可以省掉, 测给代码消费的最外层. 比如你在前端有个 view + redux store, 那只用 action 和 selector 测业务, 因为它们是. UI 可以跳过的原因是因为 UI 是给人用的, 所以很难测且不稳定, 比如发请求按钮变化, 测代码接 promise 就好了, 但是纯测 UI 就只能轮询, 一旦失败就要死等 N 秒 timeout.


四: Mock

Mock 仅用于 web server 之类没有业务意义的基础设施, 不要用于业务代码. 不然 1) mock 是错的, 或者某些细节不够真导致测试根本是错的 2) 代码僵化 3) 被 mock 模块变了, 依赖的模块没改.
YouLMAO
2021-01-28 15:33:36 +08:00
等你的代码一年创造 1 亿美金营收,你就知道 unittest 的含义
YouLMAO
2021-01-28 15:36:34 +08:00
@namelosw 你很大蝙蝠说的是集成测试 e2e integration test
sockpuppet9527
2021-01-28 16:11:41 +08:00
@YouLMAO #19 我没任何觉得 UT 不好的意思,我觉得 UT 很好,这里只是谈“过度”这件事情。
YouLMAO
2021-01-28 16:31:06 +08:00
@sockpuppet9527 你的现状应该远远没达到过度阶段吧, 看看 Apache 顶级项目, 她们其实已经是业界知名但是覆盖率还是挺低的, 比我们家的低很多
ashuai
2021-01-28 16:42:00 +08:00
为什么我第一感觉觉得这是 2077 [狗头]
namelosw
2021-01-28 16:57:59 +08:00
@YouLMAO 我说的不是集成测试, 而且我甚至建议尽量不写集成测试
petercui
2021-01-28 17:30:05 +08:00
@sockpuppet9527 UT 不需要设计,只需要产生确定的输入,然后验证一个确定的输出就可以了,如果你动了某个方法,造成了 UT 失败,那么首先需要考虑的并不是去更改 UT,而是要考虑这个方法的改动会不会造成其它组件异常,因为这个方法的行为被改变了,而且对外部系统(调用方)也造成了影响(因为你的 UT 失败了)。
YouLMAO
2021-01-28 18:21:52 +08:00
楼主,你描述的硬件驱动,明显属于集成测试了,跟 unittest 没有一毛钱关系,单元测试是不需要任何驱动的
YouLMAO
2021-01-28 18:22:50 +08:00
C 建议你下载 gmock 试试,很简单的
lolding
2021-01-28 20:21:57 +08:00
我们专门做 c 语言自动化单元测试工具,自动生成测试用例,覆盖率基本上都能够达到 100%
pkookp8
2021-01-28 20:42:49 +08:00
我一直以为 ut 是用来调试的,出了问题方便调试
更大粒度的模块测试,系统级别的测试才是用来测功能的
有时候很难为了 x==(z+y)的结果返回 true 还是 false 而去重新编译一个非常大的工程,再去重新验证
# x y z 都是临时中间变量,且产生过程比较复杂,输入值很少也很确定
如果有 ut 就方便多了

单元测试发现的问题,通常 review 就能发现。发现不了的问题很多都是边界场景没有考虑全。即使写了 ut 也无法保证功能没有问题
想想 leetcode 动辄几百几千几万个测试用例来验证你的算法
johnsona
2021-01-28 22:37:22 +08:00
不写 明天就改 写什么写
petercui
2021-01-28 22:50:37 +08:00
@pkookp8 功能测试只是一个点,但还有更重要的一个点:“重构”,重构后的代码和重构前是否行为一致,就靠测试用例来保证,只要你原有的测试用例能通过,哪怕你这个类已经没有任何一行代码跟重构之前是一样的,也不用担心会影响到调用方。
ReferenceError
2021-01-28 22:56:53 +08:00
写测试也要有个可行的 plan 排期吧。怎么感觉你在赶测试?

单说期望效果的话,测试覆盖度越全越好只要有时间是没毛病的,无法保证哪个功能一定不会出错。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/749125

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX