1. 为什么Python项目一定要补上单元测试
1.1 它到底解决什么问题
写到单元测试,我先说一个很实在的场景。某次我给一个内部数据处理服务加新功能,改完一个解析函数,自测时充分信心满满,结果上线当天就被日志里的报错打脸——一个我完全没碰过的分支,因为改动了上游返回结构,悄悄把下游逻辑带崩了。那个函数在另一个模块里被调用,而那个模块的调用者,又被第三层模块引用。Python这种动态语言,没有编译期的类型检查兜底,这种连锁反应只能靠人肉排查,而人肉排查最大的问题就是:你根本不知道哪个角落会先炸。
单元测试解决的就是这个。它不是帮你证明代码“现在没问题”,而是帮你在改动之后,快速发现“哪里出了问题”。一组写得好的用例,相当于给每个模块上了一套自检程序:输入固定数据,检查预期输出,一旦行为偏离就立刻报警。这样你改代码的时候才敢放开手脚去重构,而不是每改一行都提心吊胆地跑一遍全流程手工验证。
1.2 适合谁,以及投入产出比怎么看
如果你是个刚接触Python不久的新手,你可能会觉得“我的代码自己都跑通了,写测试不就是多写一遍吗”。这个想法我很理解,因为我当初也是这样想的。但等你真正被线上故障教育过一次之后,就会明白:手工验证只能证明“这一次”能跑通,而测试是让“每一次”都能跑通的唯一可靠手段。
如果你已经在写业务代码,甚至维护一个小型项目,那单元测试更是刚需。最简单的场景:你接手一段别人写的代码,看不懂逻辑,但又不敢随便动,因为不知道动了会坏什么。这时候一套测试就是最好的文档,它告诉你这个函数“吃进去什么、吐出来什么、边界在哪里”。所以我特别建议哪怕是个几十行的脚本项目,也顺手补几个用例,成本极低,收益却会随着项目膨胀越来越大。
2. 环境准备与第一个测试:工具链选型
2.1 工具选型:unittest、pytest、还是doctest
Python世界里测试框架选择其实不少,但我实测下来最推荐的是pytest。原因很简单:上手门槛低,断言写法直白,fixture机制好用,生态也成熟。标准库里的unittest也不是不能用,但它的类式写法偏重,断言方法名又长又绕,比如assertEqual、assertTrue这些,写着写着就感觉像是在写Java,而不是在写Python。
很多人忽略的doctest,我的态度是:可以作为文档示例用,但不建议当成主力测试手段。它把测试写在docstring里,适合验证文档里的示例是否可执行,但逻辑复杂一点的用例写在docstring里会显得很乱,而且一旦用例多了,docstring会被撑得没法看。
pytest和unittest两者之间,我建议直接用pytest。它天然兼容unittest风格的测试类,所以就算你预留了从标准库迁移的历史项目,pytest也能直接跑起来。另外pytest的fixture、参数化、插件体系,比unittest的setUp/tearDown要灵活太多。
补充说明:这里选择pytest作为讲解主线,但文中的思路对unittest同样适用。框架只是工具,测试思想才是核心。
2.2 跑通第一个测试实例
先说环境。用pip安装pytest,一条命令:
pip install pytest然后建一个示例项目,我习惯这样组织目录:
my_project/ ├── src/ │ ├── __init__.py │ ├── calculator.py └── tests/ ├── __init__.py └── test_calculator.py写一个最简单的被测试模块,比如一个计算器:
# src/calculator.py def add(a, b): return a + b def divide(a, b): if b == 0: raise ValueError("除数不能为0") return a / b再写对应的测试:
# tests/test_calculator.py import pytest from src.calculator import add, divide def test_add_positive_numbers(): assert add(1, 2) == 3 def test_add_negative_numbers(): assert add(-1, -2) == -3 def test_divide_normal(): assert divide(10, 2) == 5.0 def test_divide_by_zero(): with pytest.raises(ValueError): divide(10, 0)然后在项目根目录执行:
pytest正常情况下你会看到类似这样的输出:
collected 4 items tests/test_calculator.py .... [100%]4个测试全部通过。如果某个断言失败,pytest会非常清晰地告诉你期望值和实际值的差异,这正是它比裸print调试好用百倍的地方。
2.3 测试文件组织与命名规范
文件组织这块,我踩过一个坑:一开始把所有测试都丢在一个test_all.py里,结果项目一膨胀,跑一次测试要等好几分钟,定位失败用例也得满文件找。后来改成按模块分文件,每个被测模块对应一个测试文件,命名严格遵循test_<模块名>.py,函数命名用test_<被测函数>_<场景>,比如test_add_positive_numbers。
这样命名的好处有两个:第一,pytest默认会递归发现所有以test_开头的文件和函数,不用额外配置;第二,测试名字本身就是行为文档,你一眼就能看出某个用例覆盖的是哪个函数的哪个场景。等到测试失败的时候,看名字就能大概猜出是哪里出问题了,不用一层层去翻代码。
另外我建议在项目根目录放一个pytest.ini或pyproject.toml,把测试路径、忽略规则、覆盖率配置统一管起来。比如我常用的一个最小配置:
# pytest.ini [pytest] testpaths = tests python_files = test_*.py python_functions = test_* addopts = -q --tb=short-q是减少输出噪音,--tb=short是让traceback只显示关键行,不然一屏一屏的红字看着特别闹心。
3. 编写高质量测试:断言、用例设计与细节
3.1 断言的艺术:少用print,多用assert
很多新手写测试时有个习惯:在代码里塞一堆print,然后靠肉眼对比输出结果。这种做法最大的问题是“自动化程度为零”——你得一个人盯着控制台逐行看,测试用例再多一点,眼睛就先花了。
正确的做法是把判断交给断言。Python的assert语句就是干这个的,但它有个隐藏的坑:如果你用python -O方式运行脚本,所有assert都会被直接跳过。不过在pytest里这不构成问题,因为pytest会重写assert语句,把它变成带详细对比信息的检查逻辑。
写断言时有几个小技巧值得记一下。当你要断言两个浮点数相等时,别直接写assert a == b,浮点精度分分钟教你做人。正确姿势是:
def test_float(): assert abs(0.1 + 0.2 - 0.3) < 1e-9或者用pytest自带的方法:
import pytest def test_float(): assert pytest.approx(0.1 + 0.2) == 0.3当你要断言一个容器包含某个元素时,直接用in;要断言字典中的某个键值对时,可以直接用==比较整个字典。pytest的失败输出会打印两个对象的diff,看起来非常直观。
还有个容易被忽略的:断言异常信息是不是符合预期。比如判断除零异常,你可以更进一步验证异常文本:
def test_divide_by_zero_message(): with pytest.raises(ValueError, match="不能为0"): divide(10, 0)这样至少能防止某天你把异常类型改对了、但异常提示写得驴唇不对马嘴的问题。
3.2 一个好的用例怎么设计:边界值、异常路径、空值
这是测试里最有含金量的部分。我见过太多人写测试时只测“正常路径”,也就是传进去几个常规参数,看看结果对不对。这当然没错,但这种测试的防御能力很弱——真正促使代码在线上出bug的,几乎都是边界条件和异常分支。
以计算器里的divide函数为例,除了测试正常的整数除法、负数除法,至少还应该覆盖:
- 除数为0:必须抛异常
- 被除数为0:结果是0,但要注意浮点还是整型的0
- 除数为极小值,比如1e-300:结果会非常大,可能触发浮点上溢
- 参数类型是字符串:比如
divide("10", 2),要么明确支持,要么明确抛出TypeError,不能静默产生诡异结果
把这些边界场景列出来,你写着写着就会发现,被测试代码本身可能就有问题。比如divide("10", 2)在当前实现里会直接抛TypeError,但如果你希望能友好提示用户参数类型错误,那这里就是一个需要增强的点。
边界值分析是我每写一个测试必过的心理清单:正常值、边界值(最大/最小)、空值、None、空字符串、空列表、异常值、超长输入、重复输入。把这些都过一遍,测试用例的“防御纵深”才算建立起来。
3.3 参数化:用一份逻辑覆盖一批用例
有时候你会遇到这种情况:同一个函数,要验证的输入输出组合有十几组。如果每一个组合都写一个test函数,那代码冗余会非常严重。这时候就该上参数化。
pytest的parametrize是一个非常强大的武器。看个例子:
import pytest from src.calculator import add @pytest.mark.parametrize("a,b,expected", [ (1, 2, 3), (-1, -2, -3), (0, 0, 0), (1.5, 2.5, 4.0), (100, 200, 300), ]) def test_add(a, b, expected): assert add(a, b) == expected执行后,pytest会把每个参数组合当作独立的测试用例运行,失败时能精确定位到是哪组参数出了问题,而不是笼统地说“test_add失败”。这对于排查回归问题实在太关键了,尤其是当你有20组参数、其中1组失败的时候,如果没有参数化,你只能看到一个大红叉,完全不知道是哪组触发的。
参数化还有个进阶玩法:结合fixture和间接参数化,可以在不同环境下跑同一套逻辑。比如数据库测试时,你可以把一个“数据库连接”作为间接参数,让同一套用例在SQLite和PostgreSQL上各跑一遍。虽然这个操作稍微进阶一点,但理解了基础参数化之后,这个扩展一点也不难。
4. fixtures与依赖控制:测试不再互相干扰
4.1 fixture基本用法与作用域
fixture是pytest的灵魂,它解决了测试之间共享配置和资源的问题。一开始测试少的时候,你可能觉得每个测试里自己创建临时数据也挺好的。但等你测试变多,每个用例都要初始化一个数据库连接或临时文件时,就会发现大量的重复代码,而且测试之间还会互相影响。
fixture的基本用法就像在声明一个“前置条件”:
import pytest @pytest.fixture def sample_data(): return {"name": "test", "value": 42} def test_use_data(sample_data): assert sample_data["name"] == "test"这个简单的例子看不出fixture的威力,真正的区别在于作用域。fixture可以控制它是每个测试函数执行一次,还是每个模块执行一次,甚至整个测试会话只执行一次:
@pytest.fixture(scope="module") def db_connection(): # 建立连接,整个过程只执行一次 conn = create_connection() yield conn # 测试模块结束后统一关闭 conn.close()scope参数有四个常用值:function(默认,每个测试函数都执行)、class(每个测试类一次)、module(每个测试模块一次)、session(整个测试会话一次)。我自己的选择原则是:无状态、可重用的资源用module或session级别,有状态、模拟用户操作的资源用function级别。比如token生成、连接池,用module级别性能最好;而模拟客户端登录态,每个用例独立生成反而更稳定。
4.2 临时数据与数据库隔离
数据库测试是所有测试里最容易互相踩踏的环节。两个测试用例如果共用同一个数据库表,而它们又都对表执行了插入、修改、删除,那么你跑完用例A再跑用例B,B的结果可能完全变了。这可不是玄学,而是实实在在的“测试污染”。
我目前的方案是用临时数据库。SQLite是测试的主力,因为不需要额外启动服务,内存模式跑起来飞快。如果你测试的是真实业务逻辑而不关心特定数据库方言的语法差异,SQLite内存库是个非常好的选择。
pytest有个内置fixture叫tmp_path,它会在你每次测试时生成一个临时目录,测试结束自动清理。结合它就能做临时文件和数据文件测试:
def test_write_read_file(tmp_path): data_file = tmp_path / "data.txt" data_file.write_text("hello pytest") assert data_file.read_text() == "hello pytest"真实项目的数据库测试,建议做法是这样的:在每个测试函数开始前创建独立的数据库连接,测试结束后把所有表清空,确保下一个用例从干净的起点开始。这个fixture可以封装成这样:
@pytest.fixture(autouse=True) def clean_database(): yield # 每个测试结束之后自动执行清理 database.session.query(SomeModel).delete() database.session.commit()autouse=True的意思是,这个fixture不需要显式声明为函数参数,pytest会自动在测试前后执行。这个开关要谨慎使用,因为它对每个测试都生效,但用在“清空数据库”这个场景上,通常是最省心的方案。
4.3 依赖外部服务的模块怎么测
在实际项目中,你的代码很可能要访问外部HTTP API、消息队列、文件系统等等。如果每个测试都真的去请求外部服务,不仅要面对网络不稳定的问题,还可能因为访问频率太高被封IP。
处理这类依赖的思路有两个:一个是mock(模拟对象),一个是依赖注入(把外部服务抽象成可替换的接口)。两者不冲突,可以结合使用。依赖注入的写法示例:
# src/order_service.py class OrderService: def __init__(self, payment_gateway): self.payment_gateway = payment_gateway def process_order(self, order): result = self.payment_gateway.charge(order.amount) return result.success测试时传入一个假的支付网关,根本没有真实网络请求:
class FakePaymentGateway: def charge(self, amount): return ChargeResult(success=True) def test_process_order(): service = OrderService(FakePaymentGateway()) order = Order(amount=100) assert service.process_order(order) is True这种做法比mock更“正道”,因为它改变的是代码结构本身,让依赖关系变清晰。而mock是在测试时偷梁换柱,它不推动代码改进,只解决问题本身。两种手段各有适用场景,下文具体展开。
5. 对付外部依赖:mock与monkeypatch实战
5.1 为什么要mock
mock就是模拟对象,它替代真实的外部依赖,让你在测试时可以完全掌控依赖行为。典型场景:你的代码调用了第三方支付接口、发送短信、访问数据库,这些操作要么慢、要么贵、要么根本不具备在测试环境运行的条件。这时候mock可以替代真实调用,返回你预设的结果。
pytest里可以用unittest.mock,也可以用pytest-mock插件。我推荐pytest-mock,因为它提供的是fixture形式的mocker,写法更简洁,而且自动清理mock对象,不会串到其他测试里去。
pip install pytest-mock举个实际例子。假设你有个函数要调外部API获取天气数据,然后判断是否适合出门:
# src/weather.py import requests def is_good_weather(city): resp = requests.get(f"https://api.example.com/weather/{city}") data = resp.json() return data["condition"] in ["sunny", "cloudy"]测试时你可以直接用mocker替换requests.get:
def test_is_good_weather_sunny(mocker): mock_resp = mocker.Mock() mock_resp.json.return_value = {"condition": "sunny"} mocker.patch("src.weather.requests.get", return_value=mock_resp) assert is_good_weather("北京") is True这里强调的是:mocker.patch("src.weather.requests.get")必须patch到“被测试模块引用的位置”,而不是patch到requests库本身。因为is_good_weather里用的是src.weather.requests这个引用,如果你写mocker.patch("requests.get"),大概率不会生效,因为两个对象的地址已经分离了。这个细节是mock最常见的坑之一。
5.2 mock的常用姿势与常见误区
mock的几个核心方法必须熟练:return_value控制返回值,side_effect控制副作用(可以是一个异常,也可以是一个函数或列表),assert_called_once_with用来验证调用参数。
def test_api_retry_on_error(mocker): mock_get = mocker.patch("src.weather.requests.get") # 第一次抛异常,第二次成功 mock_get.side_effect = [requests.Timeout(), mock_resp] result = fetch_with_retry() assert mock_get.call_count == 2常见误区有三个。
第一个误区是“mock得太彻底”。有时候你把整个第三方库全mock了,断言它的返回值,但实际上代码根本没有正确处理那个返回值,测试照样能通过。这就等于只检查了“帽子还在头上”,没检查“帽子有没有戴好”。解决办法是,断言的核心不只是mock的调用,更重要的是被测试函数的输出结果。换句话说,mock的职责是控制依赖,真正的判断还是要落在最终行为上。
第二个误区是“过度使用mock,掩盖了真实接口变化”。比如第三方API的返回字段从condition改成了status,而你的测试mock数据还写着condition,那测试依然通过,但线上就会挂。这类问题没有完美答案,能做的只有定期做真实联调测试,或者抽出部分用例作为“集成测试”,不mock真实服务。
第三个误区是“忘记断言mock被正确调用”。有时候代码逻辑错误地跳过了外部调用,但测试没检查调用次数,于是静默通过。建议对关键外部调用统一补一句assert mock_get.assert_called_once_with(expected_url)。
5.3 monkeypatch什么时候更好
monkeypatch是pytest内置的fixture,它和mock的区别在于更“底层”——它直接在运行时替换模块中的属性或环境变量,而且测试结束自动还原。mock侧重于替换对象并记录调用行为,monkeypatch则更擅长修改环境变量、修改模块属性、修改类的属性等操作。
举个例子,当你的函数依赖环境变量时,monkeypatch是首选:
import os def get_debug_mode(): return os.environ.get("DEBUG", "0") == "1" def test_debug_mode_on(monkeypatch): monkeypatch.setenv("DEBUG", "1") assert get_debug_mode() is True def test_debug_mode_off(monkeypatch): monkeypatch.delenv("DEBUG", raising=False) assert get_debug_mode() is Falsemonkeypatch还能替换字典中的某个键、删除属性等等。它的好处是没有Mock对象那么多花哨的API,语义直接,而且天然和pytest的fixture机制融合得很好。
我的使用原则是:如果只是替换返回值或模拟外部API,用mock;如果是要修改环境、属性、函数实现细节,用monkeypatch。两者混用也没有问题,但别在同一份测试里一会儿mock一会儿monkeypatch去改同一个对象,那会把自己绕晕。
6. 覆盖率、CI与持续迭代:让测试真正跑起来
6.1 覆盖率怎么算、怎么读、怎么用
覆盖率是衡量测试完整度的一个重要指标,它告诉你“有多少代码行被执行过”。pytest统计覆盖率需要装插件:
pip install pytest-cov然后运行:
pytest --cov=src --cov-report=term-missing --cov-report=html--cov=src指定统计哪个目录,--cov-report=term-missing在终端输出哪些行没覆盖到,--cov-report=html生成一个HTML报告,可以浏览器打开看具体哪些行标红了。
我第一次看到覆盖率报告时的第一反应是:我明明写了十几个测试,怎么覆盖率才62%?后来才明白,覆盖率统计的是“被执行的代码行占比”,不是“被测试的函数占比”。可能你测了一个函数的所有分支,但另一个完全没有测试的函数会把整体覆盖率拉低。
所以读覆盖率报告的正确姿势是:不要只盯着总百分比,要重点看哪些关键模块覆盖率为0。如果一个支付模块从没被执行过,就算整体覆盖率80%,对你的项目来说也没有意义。我更建议把覆盖率目标按模块分开设置,比如核心业务模块要求90%以上,工具类模块80%以上,不求一把抓,但求重点不塌。
6.2 覆盖率与测试质量的关系:不只是数字游戏
覆盖率有个名声在外的悖论:100%覆盖率也可能什么都测不出来。假设一个函数是return 1 + 1,你测试写了assert f() == 2,覆盖率达到100%,但你这个测试没有任何实际意义,因为代码里根本没有分支和错误处理的逻辑。
所以我不建议团队把覆盖率当成KPI去追。覆盖率是有价值的“体检指标”,但它代替不了用例设计。一个真正高质量的测试,不仅要覆盖代码行,更要覆盖行为分支、异常路径和边界条件。
比较务实的做法是:把覆盖率作为一个回归指标,用CI随时监控。覆盖率突然大幅下降,说明某次代码变更引入了大段未测试的新逻辑,这时需要人工介入判断是否需要补充测试。这比单纯追求“覆盖率必须95%以上”要有意义得多。
6.3 接入CI与增量测试策略
本地跑测试只解决了一半问题。你无法保证每个同事都在提交代码前记着跑一遍完整的测试套件,也无法保证不同环境下测试行为完全一致。所以把测试接到CI(持续集成)上是必经之路。
主流的CI平台,本质就是“在云端的干净环境里跑你配置的脚本”。一个典型的工作流包含这几个步骤:拉取代码、安装依赖、跑测试、统计覆盖率、生成报告。配置写法因平台而异,但概念是通用的。
接入CI之后,还要考虑速度问题。如果测试全量跑要10分钟,开发者的体验会非常差。我的策略是分级:
- 提交级(pre-commit):只跑和本次改动相关的测试,也就是增量测试。可以使用pytest的
-k参数去匹配用例名,比如pytest -k "user or order",只跑和用户、订单相关的用例。 - CI全量测试:合并到主干前跑全量,通常设置在10分钟以内跑完。如果超出这个时间,就要考虑拆分测试套件、并行执行或者对测试做精简。
- 深度测试(定时任务):包含集成测试、端到端测试,跑在真实环境上,用定时触发,不在主干流程里阻塞所有人。
增量测试的一个实用技巧是,在CI里根据变更文件列表推断受影响模块。比如你改了src/calculator.py,那就跑tests/test_calculator.py;改了多个模块,就跑多个对应测试文件。很多项目会用一个脚本自动生成这份名单,让增量测试跑得更精准。
7. 常见问题与排查技巧实录
7.1 我没少踩的五个坑
先说第一个坑:测试之间状态互相污染。某次我给一个模块添加了一个全局缓存字典,结果测试A往缓存里写了一个值,测试B读出来,判断逻辑就偏了。当时花了大半天时间排查,最后发现就是一个module级别fixture没有清理干净。从那以后,我给自己立了个规矩:只要fixture里有状态变化,必须提供清理逻辑,或者用function作用域。
第二个坑:测试文件里的import路径问题。当你的项目不是用pip包安装的,而是直接在源码目录里运行时,pytest不一定能正确找到模块。解决办法是项目根目录添加conftest.py(空文件也行),pytest会自动把根目录加入sys.path;或者在pytest.ini里配置pythonpath = .。
第三个坑:把测试写得太“实现相关”。比如你的函数内部先调用parse_data再调用validate_data,你就在测试里断言这两个内部函数一定被调用。后来别人重构,把两步合并成一步,测试就崩了,但函数的外部行为完全没有变化。这种测试叫“高耦合测试”,它束缚的是实现细节而不是行为契约。建议多从“外部可观察的行为”去断言,少去管内部到底调用了谁。
第四个坑:mock了没注意函数签名。比如你mock了一个返回生成器的函数,但你的代码用列表推导式去接,结果拿到的是生成器对象,不是预期的元素列表。这个问题很隐蔽,因为测试环境的数据量小,生成器和列表的差异往往看不出来,等线上数据量一大才暴露。
第五个坑:数据库测试没有清理数据,导致随机失败。每次跑测试,有时通过、有时失败,而且失败原因千奇百怪。这种“随机失败”比“稳定失败”更可怕,因为它会让人下意识觉得是测试环境不稳定,而不是代码有bug。凡是遇到随机失败,第一反应就去找“谁在用共享的可变状态”。
7.2 排查技巧:从失败信息反向定位
当测试失败时,pytest会输出详细的测试堆栈和断言对比。很多新手面对一大堆红色信息会慌,然后直接去改业务代码。我的建议是反过来:先读失败信息,确认是“测试期望错了”还是“代码行为错了”。
两者的区分很简单:如果断言期望值是3,实际值是5,那可能是代码行为变了,也可能是测试数据没配对。第一件事不是去改代码,而是回到测试用例的数据准备部分,重新确认输入是否符合意图。如果输入确认没问题,再看代码逻辑。
我常用的排查套路是这样的:先用pytest -x让测试在第一个失败处停下来,避免后面一堆用例陪跑;再用--pdb在失败时进入调试器,直接查看现场变量值。虽然很多人觉得调试器不如print直观,但在测试里,--pdb是一个非常高效的工具,因为你可以直接在失败点检查所有局部变量。
还有一招是“最小化复现”:如果一个复杂的用例失败了,先把参数数据集缩小,看是不是某个特定数据导致的。通常缩小到一条数据时,问题原因就浮出水面了。这个过程虽然有点笨,但往往比盯着整个堆栈发呆更有效。
7.3 测试本身也要维护:别让它变成阻碍
最后说一个容易被忽视的问题:测试代码也是代码,它同样需要维护。我见过有些项目的测试代码写得比业务代码还复杂,一个测试函数里套了三个fixture、五个mock,读起来像天书。这种测试不仅没有降低维护成本,反而变成新的负担。
维护测试的基本建议:保持测试代码的简洁直白,宁可多写几行重复代码,也不要过度抽象。因为测试代码的目的是“快速定位问题”,而不是“展示设计模式”。测试里用了太复杂的继承结构或工厂方法,反而会让人看不清测试意图。
另一个建议是:定期删掉那些已经失去防护能力的测试。比如一个断言assert result is True的用例,如果无论代码怎么改,它都永远通过,那它其实已经没有了存在价值,留着只会拖慢测试速度,干扰后续排查。
8. 一点个人体会与最后的建议
写了这些年测试,我的体会是:测试最大的价值不是让代码“看起来更健壮”,而是给了你一个持续重构的安全网。没有测试的代码,就像走钢丝却没有下方的安全网,每一步都必须小心翼翼;有了测试,你才敢把钢丝换成结实的大桥,放心地大步往前走。
最后再分享一个小技巧:不要试图第一次就把所有测试写完美。更好的方式是“先写能跑通的测试,再慢慢补边界”。先让覆盖核心正向流程的测试通过,把项目的绿灯亮起来,然后每遇到一个线上bug,就把它变成一个回归测试用例,让它在测试套件里永远拦住同类问题。坚持这个习惯半年,你会发现测试套件会变得越来越有价值,而不是越来越沉重。
如果你现在刚开始写测试,别怕写得丑,先写起来再说。写得再丑的测试,也比没有测试强一百倍。