Pytest断言实战:从基础到高级的自动化测试验证技巧

发布时间:2026/6/29 5:40:00
Pytest断言实战:从基础到高级的自动化测试验证技巧 1. 项目概述为什么断言是自动化测试的“定海神针”干了这么多年自动化测试我见过太多因为断言写得不好而翻车的案例。一个测试用例跑过了页面也点了接口也调了最后报告显示“PASS”结果上线后用户反馈功能是坏的。问题出在哪十有八九是断言没写对或者压根没写全。断言Assert在自动化测试里就像是你去超市买东西后核对小票光把东西装进购物车不算完你得确认价格、数量、商品名称都对得上这才算一次成功的购物。在Python的pytest框架里assert语句就是这张“小票核对器”它负责验证代码的实际行为是否符合我们的预期。pytest之所以成为Python生态里测试框架的绝对主流除了其简洁的语法和强大的插件体系它对原生assert语句的“魔改”和增强功不可没。你不需要去记assertEquals、assertTrue、assertIn这些JUnit风格的方法名直接用Python的assertpytest就能给你提供极其丰富的失败信息。这对于快速定位问题至关重要。想象一下一个复杂的对象比较失败了如果只告诉你“AssertionError”你可能会一头雾水但pytest能告诉你具体是哪个字段、哪个列表的第几个元素不匹配这种调试体验是天壤之别。这篇总结就是把我这些年用pytest写断言踩过的坑、总结的技巧、以及那些官方文档里不会明说但极其好用的“野路子”系统地梳理一遍。无论你是刚开始写自动化测试的新手还是想优化现有测试套件的老鸟这里的内容都能让你对assert有一个全新的、更深入的认识。我们会从最基础的用法开始一直深入到自定义断言、断言重写原理、以及在API、UI测试中的实战应用目标是让你写的每一个断言都精准、可靠、易于维护。2. 断言基础从“能用”到“精通”的跨越很多人觉得assert不就是个关键字吗assert a b有什么好讲的但恰恰是这种轻视导致了测试用例的脆弱和不可靠。我们先来夯实基础看看如何把一个简单的断言写出花来。2.1 理解pytest的断言重写机制这是pytest断言体系的核心魔法也是它比unittest的assertEqual等方法更强大的根本原因。当你用python -m pytest运行测试时pytest会首先对测试模块进行“重写”Rewrite。它会解析你的源代码找到所有的assert语句并将其替换为一个更复杂的表达式。这个新表达式不仅执行比较还会在断言失败时捕获操作数比如a和b的详细信息。举个例子你写了def test_addition(): result 1 2 assert result 5直接运行这个函数你只会得到AssertionError。但用pytest运行你会看到AssertionError: assert 3 5看它自动把result的值3给打印出来了。这还只是开始。对于更复杂的比较比如列表def test_list(): a [1, 2, 3] b [1, 2, 4] assert a bpytest的输出会是AssertionError: assert [1, 2, 3] [1, 2, 4] At index 2 diff: 3 ! 4 Use -v to see the full diff它甚至告诉你是在索引2的位置发生了差异。如果你加上-v参数它会展示完整的对比差异。这个功能对于调试数据结构错误是神器。注意断言重写只对通过pytest命令运行的测试文件生效。如果你直接使用python test_file.py来运行或者文件不以test_开头、函数不以test_开头重写机制可能不会触发你将失去这些详细的错误信息。这是新手常踩的一个坑。2.2 基础断言类型的实战写法基础的比较运算符,!,,,,,in,not in,is,is not都可以直接用在assert后面。但怎么写更有可读性、更不容易出错是有讲究的。1. 相等与不相等断言# 直接比较 assert user.name “张三” # 比较前进行预处理更健壮 assert user.name.strip().lower() “zhangsan” # 对于浮点数永远不要直接用 assert abs(0.1 0.2 - 0.3) 1e-9 # 使用误差范围 # pytest提供了更优雅的方式pytest.approx import pytest assert 0.1 0.2 pytest.approx(0.3)pytest.approx是处理浮点数比较的黄金标准它可以处理绝对误差和相对误差非常灵活。2. 成员与身份断言# 检查元素是否在容器中 assert ‘admin’ in user.roles assert ‘guest’ not in user.permissions # 检查对象是否为None优先使用is/is not assert response.data is not None assert error is None # 检查两个变量是否指向同一个对象 assert obj1 is obj2 # 身份相同 assert obj1 obj2 # 值相等可能身份不同这里有个关键点is比较的是内存地址身份调用的是对象的__eq__方法值相等。对于None、True、False这类单例对象一定要用is。3. 真值断言# 检查布尔值 assert success is True assert not failure # 检查容器是否为空/非空利用Python的“真值”特性 assert shopping_cart # 等价于 len(shopping_cart) 0购物车非空 assert not error_list # 等价于 len(error_list) 0错误列表为空 # 检查字符串 assert username # 非空字符串 assert not message or message.strip() # 允许为空或空白字符串利用Python的“真值测试”Truthiness可以让断言更简洁。空列表[]、空字典{}、空字符串“”、None、数字0在布尔上下文中都被视为False。2.3 为断言添加清晰的失败信息这是提升测试可维护性的最重要技巧之一。一个光秃秃的assert失败时你可能需要花时间看上下文才知道在验证什么。加上失败信息就像给错误贴上了标签。# 不推荐的写法 assert len(items) 0 # 推荐的写法 assert len(items) 0, f“购物车商品数量应为正实际为 {len(items)}” assert user.is_active, f“用户 {user.id} 应该处于激活状态” assert ‘token’ in response.json(), f“响应中缺少token字段完整响应{response.text}”失败信息应该清晰地说明“期望什么”和“实际是什么”。使用f-string来动态嵌入实际值信息量最足。当这个断言在CI/CD流水线中失败时清晰的错误信息能让排查效率提升数倍。3. 高级断言技巧应对复杂验证场景当你的测试从简单的单元测试扩展到集成测试、API测试时断言的对象会变得非常复杂嵌套字典、对象列表、HTML片段、JSON响应。这时候基础的assert就显得力不从心了。我们需要更强大的工具。3.1 处理复杂数据结构JSON与字典断言API测试中断言响应的JSON结构是家常便饭。直接对两个大字典用比较一旦失败错误信息会非常冗长难以阅读。我们需要更精细的控制。策略一逐字段断言。这是最清晰、最可控的方式特别适合作为契约测试确保接口返回了必需的字段和正确的类型。def test_user_api(): response client.get(‘/api/user/1’) data response.json() assert response.status_code 200 assert ‘id’ in data assert isinstance(data[‘id’], int) assert data[‘id’] 1 assert ‘username’ in data assert isinstance(data[‘username’], str) assert len(data[‘username’]) 0 assert ‘email’ in data # 可以验证邮箱格式 assert ‘’ in data[‘email’] assert ‘created_at’ in data # 验证时间戳格式 assert isinstance(data[‘created_at’], str)这种写法的优点是任何一个字段出错都能立刻知道是哪个字段不符合预期。缺点是代码量稍大。策略二使用模式匹配如字典包含比较。我们经常只关心响应中的部分关键字段不关心其他字段。这时可以用字典解构或自定义函数。# 只断言关心的字段 expected_fields { ‘id’: 1, ‘username’: ‘zhangsan’, ‘is_active’: True } for key, expected_value in expected_fields.items(): assert data.get(key) expected_value, f“字段 {key} 不匹配” # 或者写一个辅助函数 def assert_dict_contains(expected_subset, actual_dict): “”“断言 actual_dict 包含 expected_subset 中的所有键值对”“” for key, expected_value in expected_subset.items(): assert key in actual_dict, f“缺少键{key}” assert actual_dict[key] expected_value, f“键 {key} 的值不匹配。期望{expected_value}实际{actual_dict[key]}” assert_dict_contains({‘status’: ‘success’, ‘code’: 0}, response.json())策略三使用专门的断言库如pytest-assert-utils或deepdiff。对于极其复杂的深度比较这些库是救星。# 使用 deepdiff 进行深度比较并生成可读的差异报告 from deepdiff import DeepDiff expected {‘user’: {‘profile’: {‘name’: ‘张三’, ‘age’: 30}}} actual {‘user’: {‘profile’: {‘name’: ‘张三’, ‘age’: 31, ‘city’: ‘北京’}}} diff DeepDiff(expected, actual, ignore_orderTrue) assert not diff, f“数据结构存在差异{diff}” # 如果存在差异diff会是一个包含详细信息的字典例如 # {‘values_changed’: {“root[‘user’][‘profile’][‘age’]”: {‘new_value’: 31, ‘old_value’: 30}}, ‘dictionary_item_added’: [“root[‘user’][‘profile’][‘city’]”]}deepdiff能告诉你具体是值变了、项增加了还是删除了在对比配置、复杂API响应时非常有用。3.2 异常断言验证代码是否按预期抛出错误测试错误处理路径和边界条件是保证代码健壮性的关键。pytest提供了非常优雅的异常断言方式。import pytest def test_division_by_zero(): “”“测试除以零应抛出ZeroDivisionError”“” with pytest.raises(ZeroDivisionError): result 1 / 0 def test_invalid_input(): “”“测试传入非法参数应抛出特定异常并可检查异常信息”“” with pytest.raises(ValueError) as exc_info: # 捕获异常对象 int(‘invalid’) # 断言异常信息中包含特定文本 assert ‘invalid literal’ in str(exc_info.value) # 或者直接断言异常信息 # exc_info.value 就是捕获到的 ValueError 实例 assert exc_info.value.args[0] “invalid literal for int() with base 10: ‘invalid’” def test_custom_exception(): “”“测试自定义异常”“” class InsufficientFundsError(Exception): pass def withdraw(amount, balance): if amount balance: raise InsufficientFundsError(f“余额不足。当前余额{balance}尝试提取{amount}”) return balance - amount with pytest.raises(InsufficientFundsError, matchr“余额不足.*余额100.*提取200”): withdraw(200, 100)pytest.raises作为一个上下文管理器其内部的代码块必须抛出指定的异常测试才会通过。match参数可以使用正则表达式来匹配异常信息这能确保抛出的异常不仅是正确的类型信息内容也符合预期。exc_info对象可以让你在断言后继续检查异常的详细信息非常强大。实操心得在测试异常时一定要确保pytest.raises块内的代码确实会抛出异常。我见过有人写了with pytest.raises(...): some_code()但some_code()其实永远不会抛异常导致测试错误地通过了。这是一种静默的测试漏洞。对于重要的异常路径可以故意先让测试失败确认异常断言机制在工作再修复代码让测试通过。3.3 使用pytest内置的断言辅助函数除了重写assertpytest还提供了一些函数来应对特殊场景。pytest.approx前面提过用于浮点数比较。它支持相对容差和绝对容差。assert 99.9 pytest.approx(100, rel1e-2) # 相对容差1%通过 assert 99.9 pytest.approx(100, abs0.1) # 绝对容差0.1通过 assert 99.9 pytest.approx(100, abs0.01) # 绝对容差0.01失败 # 对于容器内的浮点数也适用 assert [0.1 0.2, 1.0/3.0] pytest.approx([0.3, 0.3333333])pytest.fail主动让测试失败并给出原因。通常用在条件判断中当测试不应该走到某个分支时。def test_complex_condition(): result some_complex_operation() if result.status ‘error’: # 如果状态是error我们期望error_code不为空 if not result.error_code: pytest.fail(f“当状态为error时error_code不应为空。结果{result}”) elif result.status ‘success’: assert result.data is not None else: # 出现了未预期的状态 pytest.fail(f“未预期的状态值{result.status}”)这比写一堆复杂的assert和if-else逻辑更清晰失败信息也更直接。pytest.warns用于断言代码会发出警告Warning类似于pytest.raises。import warnings import pytest def test_deprecated_function(): with pytest.warns(DeprecationWarning, match“此函数已弃用”): call_deprecated_function()这在测试库的版本兼容性或迁移路径时很有用。4. 断言在UI与API自动化测试中的实战模式理论说再多不如看实战。自动化测试最终要落地到具体场景。下面我们看看在Web UI测试以Selenium/Playwright为例和API测试中如何高效、稳健地使用断言。4.1 UI自动化测试中的断言策略UI测试的断言核心是等待元素达到预期状态后再断言。直接断言而不等待是UI测试不稳定的最主要原因。反模式# 不稳定的写法 element driver.find_element(By.ID, “submit-btn”) assert element.is_enabled() # 页面可能还没加载完元素可能尚未可点击正解使用显式等待Explicit Wait结合断言。from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def test_login_button_state(): # 等待元素出现并可见 username_field WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.ID, “username”)) ) username_field.send_keys(“testuser”) password_field driver.find_element(By.ID, “password”) password_field.send_keys(“password”) # 关键等待登录按钮变为可点击状态然后断言 login_button WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, “login-btn”)) ) # 此时再断言是稳健的 assert login_button.is_enabled() assert login_button.text “登录” login_button.click() # 断言登录后的页面跳转或元素出现 WebDriverWait(driver, 10).until( EC.url_contains(“/dashboard”) ) welcome_message driver.find_element(By.CLASS_NAME, “welcome-msg”) assert “testuser” in welcome_message.text在Playwright中模式类似但语法更简洁# Playwright 示例 def test_playwright_assert(page): page.goto(“/login”) page.fill(“#username”, “testuser”) page.fill(“#password”, “password”) # Playwright 自带自动等待通常不需要额外的WebDriverWait login_button page.locator(“#login-btn”) # 断言前可以确保元素是可见、可用的 expect(login_button).to_be_enabled() # Playwright的断言库 expect(login_button).to_have_text(“登录”) login_button.click() # 断言导航 expect(page).to_have_url(“.*/dashboard”) expect(page.locator(“.welcome-msg”)).to_contain_text(“testuser”)UI断言的最佳实践断言文本内容时使用部分匹配in而非完全匹配避免因空格、换行或微小改动导致测试失败。assert “订单号” in message_text # 好 assert message_text “订单号123456” # 脆弱可能多一个空格就失败断言元素状态可见、可点击、被选中前务必等待。使用框架提供的等待机制不要用time.sleep。对于列表或表格断言数量或特定项的存在。rows page.locator(“table tbody tr”) assert rows.count() 5 # 断言是否存在包含特定文本的行 assert any(“待发货” in row.text_content() for row in rows.all())截图辅助断言对于复杂的UI状态有时断言失败信息不够直观。可以在断言失败时自动截图这是定位UI问题的利器。可以通过pytest的钩子函数或try...except实现。4.2 API接口自动化测试的断言要点API测试的断言对象主要是HTTP响应状态码、响应头、响应体JSON/XML/文本。一个健壮的API测试断言示例import requests import pytest def test_create_user(): url “/api/users” payload {“name”: “李四”, “email”: “lisiexample.com”} headers {“Authorization”: “Bearer token123”} response requests.post(url, jsonpayload, headersheaders) # 1. 断言状态码这是最基本的 assert response.status_code 201, f“预期状态码201实际为{response.status_code}响应体{response.text}” # 2. 断言响应头如Content-Type, Location等 assert response.headers[“Content-Type”] “application/json; charsetutf-8” # 创建资源后Location头通常包含新资源的URL assert “Location” in response.headers new_user_url response.headers[“Location”] assert “/api/users/” in new_user_url # 3. 解析并断言响应体 response_data response.json() assert isinstance(response_data, dict) assert “id” in response_data assert isinstance(response_data[“id”], (int, str)) assert response_data[“name”] payload[“name”] assert response_data[“email”] payload[“email”] assert “created_at” in response_data # 4. 业务逻辑断言例如新创建的用户状态应为‘active’ assert response_data.get(“status”, “active”) “active” # 5. 可选使用返回的数据进行后续操作验证数据一致性 # 例如立刻调用GET接口确认资源确实被创建 get_response requests.get(new_user_url, headersheaders) assert get_response.status_code 200 assert get_response.json()[“id”] response_data[“id”]API断言的核心技巧状态码是第一位2xx代表成功4xx代表客户端错误5xx代表服务端错误。断言要精确不要只断言response.ok。处理动态数据像id、created_at、token这种每次请求都会变的数据不要断言其具体值而是断言其存在性和类型。assert “token” in response.json() assert isinstance(response.json()[“token”], str) assert len(response.json()[“token”]) 10使用JSON Schema进行结构验证对于大型、复杂的API响应使用jsonschema库来验证响应结构是否符合预定模式比写一堆assert语句更强大、更易维护。import jsonschema user_schema { “type”: “object”, “required”: [“id”, “name”, “email”], “properties”: { “id”: {“type”: “integer”}, “name”: {“type”: “string”}, “email”: {“type”: “string”, “format”: “email”} } } jsonschema.validate(instanceresponse.json(), schemauser_schema)断言性能可选对于关键接口可以加入响应时间的断言。import time start time.time() response requests.post(url, jsonpayload) end time.time() assert (end - start) 1.0 # 响应时间应小于1秒5. 打造可维护的断言模式、封装与最佳实践当测试用例成百上千时散落在各处的assert语句会成为维护的噩梦。重复的断言逻辑、不清晰的失败信息、对数据结构变化的脆弱性都会暴露出来。我们需要像组织生产代码一样来组织我们的断言。5.1 封装自定义断言函数将重复的、复杂的断言逻辑封装成函数是提升代码复用性和可读性的第一步。# conftest.py 或专门的断言模块中 def assert_valid_user(user_dict): “”“断言一个字典符合用户对象的基本结构”“” required_fields [‘id’, ‘username’, ‘email’] for field in required_fields: assert field in user_dict, f“用户对象缺少必需字段{field}” assert isinstance(user_dict[‘id’], int), f“id应为整数实际是 {type(user_dict[‘id’])}” assert ‘’ in user_dict[‘email’], f“邮箱格式无效{user_dict[‘email’]}” assert len(user_dict[‘username’]) 3, “用户名至少3个字符” def assert_http_ok(response): “”“断言HTTP响应为2xx成功并打印失败详情”“” assert 200 response.status_code 300, \ f“请求失败状态码{response.status_code}URL{response.url}响应体{response.text}” def assert_list_not_empty(item_list, list_name“列表”): “”“断言一个列表非空并提供友好的错误信息”“” assert item_list, f“{list_name} 不应为空” assert isinstance(item_list, list), f“{list_name} 应为列表类型”在测试中使用这些封装好的断言代码会干净很多def test_get_users(): response client.get(‘/api/users’) assert_http_ok(response) users response.json() assert isinstance(users, list) for user in users: assert_valid_user(user)5.2 利用pytest的钩子增强断言失败报告有时默认的断言失败信息还不够。比如在UI测试中我们希望在断言失败时自动截屏。这可以通过pytest的钩子函数pytest_assertrepr_compare或pytest_runtest_makereport来实现但更简单的是在测试函数内部使用try...except。一个更实用的模式是创建自定义的断言上下文管理器import pytest from contextlib import contextmanager from selenium import webdriver contextmanager def assert_with_screenshot(driver, test_name): “”“一个上下文管理器如果块内断言失败则自动截图”“” try: yield except AssertionError as e: # 断言失败时截图并附加到异常信息中 screenshot_path f“./screenshots/assert_fail_{test_name}.png” driver.save_screenshot(screenshot_path) # 将原始异常信息和截图路径一起抛出 raise AssertionError(f“{e}\n断言失败截图已保存至{screenshot_path}”) from e # 在测试中使用 def test_ui_functionality(): driver webdriver.Chrome() driver.get(“...”) with assert_with_screenshot(driver, “test_ui_functionality”): element driver.find_element(...) assert element.text “预期文本” # ... 其他断言这个技巧虽然简单但在调试难以复现的UI问题时能救命。5.3 断言与测试数据分离不要让断言语句里塞满硬编码的测试数据。将测试数据尤其是期望值提取到外部文件或变量中。# 不好的做法 def test_product(): product get_product(123) assert product[“name”] “智能手机旗舰版 X100” assert product[“price”] 3999 assert product[“category”] “电子产品” # 好的做法 EXPECTED_PRODUCT_123 { “name”: “智能手机旗舰版 X100”, “price”: 3999, “category”: “电子产品” } def test_product(): product get_product(123) for key, expected_value in EXPECTED_PRODUCT_123.items(): assert product.get(key) expected_value, f“产品字段 {key} 不匹配”或者使用pytest的参数化功能将测试数据和断言逻辑分离得更彻底import pytest pytest.mark.parametrize(“product_id, expected_name, expected_price”, [ (123, “智能手机旗舰版 X100”, 3999), (456, “无线蓝牙耳机”, 299), (789, “笔记本电脑”, 5999), ]) def test_products(product_id, expected_name, expected_price): product get_product(product_id) assert product[“name”] expected_name assert product[“price”] expected_price5.4 避免常见的断言陷阱过度断言Assertion Roulette一个测试函数里塞了十几个assert一旦失败你很难快速知道是哪一个失败了。尽管pytest会指出失败的行号但逻辑过于复杂仍不利于排查。建议一个测试函数专注于验证一个逻辑点或一个场景。如果确实需要多个断言确保它们高度相关并使用清晰的失败信息。依赖测试执行顺序测试断言依赖于前一个测试创建或修改的全局状态。这是自动化测试的大忌会导致测试结果不可靠。必须保证每个测试都是独立的。使用setup_method/teardown_method或fixture来管理测试环境。断言不稳定的条件例如断言页面加载的“精确时间”、断言网络请求的“绝对顺序”、断言包含当前时间戳的字段。解决方案断言范围如assert elapsed_time 2.0断言相对顺序或者使用Mock/Stub来固定时间。忽略断言后的清理特别是在集成测试中一个断言失败后测试函数可能提前退出导致后续的清理代码如删除测试数据、关闭连接没有执行。使用try...finally或pytest的fixture来确保清理一定会执行。def test_with_resource(): resource acquire_expensive_resource() try: # ... 你的测试和断言 assert resource.status “ready” finally: # 无论断言成功还是失败都会执行清理 release_resource(resource)6. 调试与排查当断言失败时该怎么办即使有了最好的实践断言依然会失败。失败不可怕可怕的是面对失败信息无从下手。下面是一个系统化的排查流程。6.1 解读pytest的断言失败输出首先要会看pytest给出的信息。假设一个失败断言AssertionError: assert {‘name’: ‘Alice’, ‘age’: 25} {‘name’: ‘Alice’, ‘age’: 26} Differing items: {‘age’: 25} ! {‘age’: 26}它清晰地告诉你是age字段的值不匹配。如果结构更复杂pytest会进行递归比较并输出差异树。养成习惯首先仔细阅读pytest输出的第一行和最后几行差异通常在那里。6.2 使用pytest的-v和-s参数-v(verbose): 输出更详细的信息包括每个测试的名字和状态。对于参数化测试它会显示每组参数。-s: 关闭输出捕获所有print语句和标准输出都会显示在控制台。这在调试时非常有用你可以在测试中打印中间变量值。pytest test_file.py -v -s6.3 在断言前打印调试信息当复杂的断言失败时在断言前打印出实际值是最直接的调试方法。def test_complex_data(): actual some_complex_calculation() expected load_expected_data() # 调试先打印出来看看 print(f“Actual: {actual}”) print(f“Expected: {expected}”) # 或者使用pprint美化输出 import pprint pprint.pprint(actual, indent2) assert actual expected运行测试时加上-s参数你就能看到这些打印信息。6.4 使用pdb进行交互式调试对于特别棘手的失败可以引入Python调试器pdb。def test_buggy_function(): result buggy_function() import pdb; pdb.set_trace() # 在这里设置断点 assert result 42运行测试时程序会在pdb.set_trace()处暂停进入交互式调试环境。你可以使用命令检查变量p result、单步执行n、进入函数s等。输入c继续运行q退出。6.5 常见断言失败模式速查表失败现象可能原因排查步骤AssertionError但信息简略未使用pytest运行或文件/函数命名不符合pytest规则1. 确认使用pytest命令运行。2. 确认测试文件以test_开头函数以test_开头。浮点数比较失败浮点数精度问题使用pytest.approx()进行比较。assert a in b失败但肉眼看着有空格、换行符、大小写不一致打印repr(a)和repr(b)查看原始字符或统一进行.strip().lower()处理。字典比较失败但键值好像一样值的类型不同如‘123’vs123或嵌套结构不一致使用type()检查类型使用DeepDiff进行深度差异比较。UI测试中元素状态断言失败页面未加载完成/元素状态未稳定在断言前加入显式等待WebDriverWait。API测试中断言响应字段失败接口返回结构变化或字段名拼写错误1. 打印完整的响应体 (response.text)。2. 使用.get()方法安全访问字段避免KeyError。pytest.raises未捕获到异常异常类型不匹配或代码块确实未抛出异常1. 检查异常类型是否完全一致包括自定义异常。2. 确认with块内的代码逻辑确实会走到抛出异常的分支。断言是测试的灵魂一个精心编写的断言不仅能发现bug更能作为代码行为的活文档。从今天起不要再把assert当成一个简单的检查开关而是把它当作一个与代码对话、明确表达预期行为的工具。花时间优化你的断言你的测试套件会回报你以稳定、可靠和高效的缺陷捕获能力。