写测试是门艺术. 不写测试不好, 大部分项目写不好效果更差. 我觉得重要的就几个方面, 但是更多的需要实践和思考, 特别是在团队中的实践(因为很多写测试在所有人脑袋里的含义都不一样, 很多时候即便测试策略很好, 最终执行也会很差), 你可以结合你的经验思考一下:
一: 测试要和代码一起写. 好的测试都需要适合测试的代码配合(并发, 沙盒, 替换, 生成测试等等). 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 模块变了, 依赖的模块没改.