Pytest 是我这几年用得最顺手的 Python 测试框架,没有之一。从刚接触自动化测试时只会写assert断言,到后来用动态参数化把几百条测试数据压进同一个用例,再到自己写钩子扩展框架行为,这条路走下来,我踩过的坑、绕过的弯,基本都沉淀在这篇文章里了。如果你是刚准备入坑 Pytest 的新手,或者已经写了一阵子测试、想更进一步玩转参数化和插件生态,这篇文章就是给你准备的。我会从最基础的选择逻辑讲起,一层层拆到动态参数化和插件扩展的实战细节,最后附上我实测中遇到的高频问题排查记录。
1. 项目核心思路:为什么选择 Pytest,而不是 unittest
1.1 测试框架选型的三个关键因素
很多 Python 开发者一开始接触的测试框架是 unittest,毕竟它是标准库自带,零安装成本。但真正项目跑起来之后,unittest 的限制会越来越明显:类继承结构写起来啰嗦,setUp/tearDown一对方法管理资源不够灵活,断言方法名assertEqual、assertTrue写多了总觉得不够"Pythonic"。Pytest 能把这些问题一次性解决,核心原因有三个。
第一个是断言语法的亲和力。Pytest 直接用 Python 原生的assert语句做断言,失败信息由框架自动解析并展示,不需要记一堆assertXXX方法名。简单说,你在普通 Python 代码里怎么写判断,测试里就怎么写,学习成本几乎为零。
第二个是 fixture 机制带来的资源管理能力。unittest 里要在类的多个测试方法之间共享某个资源,只能靠setUpClass或模块级变量,写起来既别扭又容易漏清理。Pytest 的 fixture 是函数级别的依赖注入,通过yield把"前置准备"和"后置清理"放在同一个函数里,一眼就能看出资源生命周期。
第三个是插件生态。Pytest 官网收录的第三方插件数量超过一千个,覆盖测试报告、并发执行、覆盖率统计、Web 自动化集成等常见需求。这一点是 unittest 完全不具备的优势——你几乎总能找到一个现成插件,而不是自己重复造轮子。
1.2 Pytest 的核心理念:约定优于配置
Pytest 使用"约定优于配置"的设计思路,只要你遵循命名和目录规范,几乎不需要额外的配置文件。默认情况下,测试文件命名为test_*.py或*_test.py,测试函数命名为test_*,测试类命名为Test*,框架就会自动发现并收集这些用例。
这种约定带来的直接好处是项目结构清爽。比如你的项目目录里有一个tests/文件夹,里面丢几个遵循命名规范的文件,执行pytest命令就能自动运行,不需要像某些框架那样在配置里逐个注册用例。我自己在接手过的几个项目中都验证过一件事:凡是能坚持这个约定的团队,测试代码的可读性普遍比用其他框架要高,因为任何人打开一个测试文件,第一眼就能看出哪些函数是测试用例、哪些是辅助函数。
不过约定归约定,Pytest 也留了足够的自定义空间。你可以在根目录放一个pytest.ini或pyproject.toml,通过[tool.pytest.ini_options]配置测试路径、忽略规则、注册标记等。这里有一个很实用的经验:如果一个项目的测试文件分散在多个目录,建议在pytest.ini里明确写testpaths = tests,并打开addopts = -ra,这样每次跑测试时能清楚看到 skipped、xfailed 和 failed 的完整原因,排查问题会省很多时间。
以下是 unittest 与 Pytest 在几个核心维度上的对比,我刚转 Pytest 时就是靠这张表说服团队切换的:
| 对比维度 | unittest | Pytest |
|---|---|---|
| 断言风格 | assertEqual、assertTrue 等方法名 | 原生 assert,失败自动展示上下文 |
| 依赖管理 | setUp/tearDown,继承式共享 | fixture 函数式依赖注入 |
| 插件生态 | 几乎没有 | 超过 1000 个插件 |
| 测试收集 | 手动套件或 discover | 按命名规范自动收集 |
| 参数化支持 | 依赖 subTest,写法笨重 | parametrize 装饰器,天然支持 |
| 报告输出 | 文本简单输出 | 支持 html、allure、junitxml 等丰富格式 |
2. 从基础断言到 Fixture 机制:测试脚手架的进阶用法
2.1 基础断言与异常测试的写法
Pytest 的基础用法看起来很简单,但越简单的东西越有讲究。先看一个最典型的例子:
def add(a, b): return a + b def test_add(): assert add(1, 2) == 3 assert add(0, 0) == 0 assert add(-1, 1) == 0这样写没问题,但它只覆盖了正常路径。真正合格的测试还要验证异常情况,比如传入不支持的类型时,代码是否正确抛出TypeError:
import pytest def test_add_with_invalid_type(): with pytest.raises(TypeError): add("1", 2)pytest.raises是个非常实用的上下文管理器,它不仅能断言异常类型,还能捕获异常实例进行二次验证:
def test_add_with_error_message(): with pytest.raises(TypeError) as exc_info: add("1", 2) assert "unsupported operand" in str(exc_info.value)我见过很多新手只写"正常路径"测试,异常分支完全裸奔。结果上线后往往是被异常打崩,而不是功能退回。所以这里想强调一个实操习惯:每个核心函数至少写一个正常用例、一个边界用例、一个异常用例。Pytest 对异常测试的支持足够顺手,多写一个pytest.raises并不费事,却能在关键时刻兜底。
2.2 Fixture:测试代码里的"依赖注入"
Fixtue 可以理解成一个"带清理逻辑的前置函数"。举个生活化的例子:每次测试你都需要开一个数据库连接,测试完了再关掉。如果按 unittest 的写法,你得在setUp里开连接、在tearDown里关连接,如果某个类有多个测试方法,这套逻辑要么反复执行,要么通过类级方法共享,边界情况一多就混乱。
Pytest 的 fixture 写法是这样:
import pytest @pytest.fixture def db_connection(): conn = create_connection() yield conn conn.close() def test_query_user(db_connection): result = db_connection.query("SELECT * FROM users") assert len(result) > 0重点看yield那行:yield之前的部分是"前置准备",yield之后的部分是"后置清理"。函数执行到yield时,把conn交给测试函数使用;测试函数运行结束后,无论成功还是失败,都会回到yield后面执行清理。这种"用同一个函数管理申请和释放"的写法,比分散在setUp和tearDown里要清晰得多。
fixture 还可以指定scope参数,控制复用范围:
scope="function"(默认):每个测试函数执行前后都重新生成。scope="class":同一个测试类里的用例共享同一个实例。scope="module":同一个模块共享一个实例。scope="session":整个测试会话只生成一次,适合耗时的操作,比如登录 token 获取。
这里有一个非常常见的性能优化手段:把耗时的登录操作、初始化操作放到session级别的 fixture 里,只执行一次,而不是每个用例都登录一次。我在实测过的某个 Web 自动化项目里,200 个用例原本需要跑 30 分钟,把登录改成 session 级 fixture 后直接砍半到 15 分钟,效果立竿见影。
但 session 级 fixture 有个隐性风险——状态污染。如果某个用例修改了共享对象的内部状态,后续用例会拿到"被改过"的对象。如果遇到这种问题,要么把 fixture 降级为 module 级,要么在用例里做事后重置。关于这类问题,我在文章第 5 节会展开讲排查思路。
2.3 conftest.py:跨文件共享 fixture 的枢纽
fixture 定义在某个测试文件里,默认只能在该文件内使用。如果多个测试文件都需要同一个登录操作,把 fixture 重复写到每个文件里显然不合理。Pytest 的解决方案是conftest.py:放在某一层目录下的conftest.py中定义的 fixture,可以被该目录及所有子目录下的测试文件共享。
conftest.py的作用范围是"目录层级"而不是"包层级",它不需要__init__.py,只要文件在对应目录下就会自动被 Pytest 识别加载。这个文件常被用来放三类东西:
- 全局共享的 fixture,比如
client、session。 - 自定义的命令行选项,通过
pytest_addoption钩子注册。 - 钩子函数重写,比如修改测试收集逻辑、报告输出逻辑。
刚开始用conftest.py时容易犯一个错误:把conftest.py当作普通工具模块,在里面写一堆辅助函数然后到处import。实际这类共享代码更适合放到项目里的utils包,conftest.py应该专注做 Pytest 相关的环境装配和钩子扩展。如果conftest.py里既有工具函数又有 fixture,文件会越来越乱。
3. 动态参数化实战:让测试数据自己"说话"
3.1 parametrize 基础用法与参数组合
参数化测试是 Pytest 最亮眼的功能之一。它的核心场景是:测试逻辑完全一样,只是输入输出数据不同。没有参数化的时候,你只能把多组数据写成多个用例,或者在单个用例里用 for 循环人为组装,但 for 循环的问题在于:一旦中间某组数据失败,后面的数据不会继续执行,而且你无法从测试报告里直观看出是哪组数据出了问题。
@pytest.mark.parametrize解决了这个问题:
import pytest @pytest.mark.parametrize("a, b, expected", [ (1, 2, 3), (0, 0, 0), (-1, 1, 0), (100, 200, 300), ]) def test_add(a, b, expected): assert add(a, b) == expected当test_add执行时,Pytest 会为每个参数元组生成独立的测试节点,报告中会显示类似test_add[1-2-3]、test_add[0-0-0]的用例 ID。哪个参数组合失败一目了然,而且各组数据之间互不影响。
多参数组合时,还可以用多个装饰器形成笛卡尔积:
@pytest.mark.parametrize("user_type", ["admin", "guest"]) @pytest.mark.parametrize("endpoint", ["/api/users", "/api/orders"]) def test_access_control(user_type, endpoint): # 这里会执行 2 x 2 = 4 个用例 ...这种组合方式在权限测试里非常实用。我实测过一个登录权限模块,用两组参数(角色、接口路径)组合出几十个用例,代码只写了十几行,覆盖维度比手工逐个写用例要全得多。
如果你期望某些参数组合被跳过,可以用pytest.param配合marks:
@pytest.mark.parametrize("a, b, expected", [ (1, 2, 3), pytest.param(0, 0, 0, marks=pytest.mark.xfail(reason="边界值待修复")), ]) def test_add(a, b, expected): assert add(a, b) == expected3.2 动态生成参数:从文件、接口和数据库读取数据
@pytest.mark.parametrize的装饰器参数是一个普通的 Python 可迭代对象,这意味着你不一定非得把数据硬编码在装饰器里,完全可以写一个函数,从外部数据源读取并返回参数列表。
我最常遇到的场景是"从 CSV 文件读取测试数据":
import csv def load_test_data_from_csv(path): with open(path, encoding="utf-8") as f: reader = csv.DictReader(f) return [(row["a"], row["b"], row["expected"]) for row in reader] @pytest.mark.parametrize("a, b, expected", load_test_data_from_csv("test_data.csv")) def test_add_from_csv(a, b, expected): assert add(a, b) == expected这里要注意一个细节:parametrize的装饰器是在模块导入时求值的,所以load_test_data_from_csv会在收集阶段就执行一次。如果文件路径写错或者数据格式有问题,会在测试收集阶段直接报错,而不是运行时报错。这个行为在调试时其实很有帮助,能第一时间暴露数据问题。
除了从文件读取,动态参数还可以来自接口响应或数据库查询,比如从 CI 配置里读取运行环境分支,或者从一个在线配置中心拉取开关数据。核心逻辑都一样:你只需要保证parametrize拿到的是一个可迭代的参数列表。
3.3 进阶:用间接参数化驱动 fixture 动态行为
parametrize里有个容易被忽略的参数indirect,它非常强大。设置indirect=True后,传入的fixture参数会被当作 fixture 名来处理,而不是直接把值传给测试函数。什么意思?看例子:
import pytest @pytest.fixture def user(request): user_type = request.param if user_type == "admin": return create_admin_user() elif user_type == "guest": return create_guest_user() else: raise ValueError(f"unknown user type: {user_type}") @pytest.mark.parametrize("user", ["admin", "guest"], indirect=True) def test_user_permissions(user): # user 已经是 fixture 返回的对应用户对象 assert user.role in ["admin", "guest"]这样写的好处是,夹具可以根据参数值动态决定自己该准备什么资源,而测试函数拿到的永远是"准备好的结果",不用自己在函数里写 if-else 判断。这个模式在 Web UI 测试里尤其常见:一个 fixture 根据浏览器参数启动不同浏览器,配合多组参数一次跑完 Chrome、Firefox、Edge 的兼容性用例。
还有更细粒度的玩法,可以通过pytest_generate_tests钩子动态生成测试用例。这个钩子会在收集阶段被调用,可以在里面根据自定义标记动态调用metafunc.parametrize。举个例子,如果你的测试数据散落在数据库里,每次新增数据都不希望改动测试代码,就可以用这个钩子实现"数据驱动"的自动化收集:
def pytest_generate_tests(metafunc): if "case_data" in metafunc.fixturenames: cases = fetch_cases_from_db() metafunc.parametrize("case_data", cases, ids=[f"case:{c.id}" for c in cases])这种写法我实际用在一个接口回归项目里,测试数据存放在 MySQL 中,运营同学可以直接改数据库来新增回归场景,完全不需要碰测试代码。测试代码与数据彻底解耦,也就是"数据驱动测试"的真正奥义。
3.4 参数化运行的几个"坑":ids、中文乱码与数据爆炸
参数化虽然香,但用多了总会遇到几个高频问题。
第一个是中文 ids 乱码。参数化后测试用例 ID 默认取参数值的 repr,如果参数是中文字符串,终端和 HTML 报告里可能显示成\u7528\u6237之类的转义序列,非常难读。解决办法是手动指定ids参数:
@pytest.mark.parametrize("username", ["张三", "李四"], ids=["zhangsan", "lisi"]) def test_user(username): ...如果你希望报告中直接显示中文原文,可以在pytest.ini里设置合适的编码选项,或者用ids传入一个返回中文的函数。实测下来最稳妥的还是显式给ids,一劳永逸,不依赖环境编码。
第二个是数据爆炸。参数组合是乘法增长的,两个参数各 5 个取值就是 25 个用例,如果再加一个参数 4 个取值就变成 100 个。用例数暴增之后,测试执行时间、报告体积都会显著上升。这时候建议结合业务重要性做数据裁剪,优先覆盖真正常用的组合,而不是盲目追求全排列。如果确实要跑大量组合,可以参考第 4 节的pytest-xdist并行方案。
第三个是 fixture 与参数同名冲突。给参数起的名字如果和已有 fixture 重名,indirect=True时行为会变得很难理解。我的习惯是给参数名加前缀,比如user_id、api_path,尽量避免与 fixture 名重复。
4. 插件扩展:从 conftest 到自定义插件的进阶之路
4.1 高频实用插件实测
Pytest 的插件生态是它的另一大杀手锏。这里说的插件,就是通过pip安装的第三方包,安装完成后 Pytest 会自动发现并激活。
实测下来,五个插件对我的项目帮助最大:
pytest-html:生成 HTML 格式测试报告,自带时间轴和失败详情截图位置,适合团队内部预览。pytest-xdist:多进程并行执行测试,通过-n auto参数自动利用 CPU 核心数,处理大量用例时提速明显。pytest-cov:统计测试覆盖率,支持--cov=my_project --cov-report=html输出可读的 HTML 报告。pytest-ordering:通过@pytest.mark.run(order=1)指定用例执行顺序,适用于有依赖关系的集成测试。allure-pytest:接入 Allure 报告框架,生成带步骤、截图、附件和层级结构的专业测试报告。
安装方式很简单:
pip install pytest-html pytest-xdist pytest-cov pytest-ordering allure-pytest执行方式也很直观:
pytest -n auto --html=report.html --cov=my_project --cov-report=html -v这里要注意,并非所有插件都能和xdist完美兼容。某些依赖"单进程采集全量信息"的插件在并行模式下会出现数据不完整的问题。比如pytest-html在多进程下如果没配置好每一进程写不同文件,最终报告可能只包含一部分用例。我实测下来的解决办法是:并行跑测试时,各进程分别生成独立的 XML 结果文件,最后用一个汇总脚本合并。或者降低预期,串行跑报告类任务。
4.2 conftest.py 里自定义钩子的实战
除了装第三方插件,Pytest 还允许你在conftest.py中重写钩子函数,这可以理解成"自己写插件"的轻量版。
最常见的钩子是pytest_addoption和pytest_configure,用于注册自定义命令行参数:
def pytest_addoption(parser): parser.addoption("--env", action="store", default="test", help="运行环境: test/staging/prod") def pytest_configure(config): env = config.getoption("--env") config._env = env然后在 fixture 里读取这个配置:
@pytest.fixture def env_config(request): env = request.config._env return load_config(env)这个模式在接口自动化里非常好用:你不需要每次改代码切环境,只要命令里带--env=prod就能切换整套 API 地址、账号数据。我接手过的几个项目里,光靠这个自定义参数就省掉了大量"上线前改 URL"的低级操作。
另一个常用钩子是pytest_runtest_makereport,它可以在用例执行结束后拿到成功/失败的结果对象,用于自动截图、收集日志。举个 Web 自动化的例子:
import pytest @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: driver = item.funcargs.get("driver") if driver: driver.save_screenshot(f"{item.name}.png")这个钩子最大的价值是:无论用例因为什么原因失败,都能自动留下现场截图,不用你在每个用例里手动加 try-except。
4.3 PO 模式与 Allure 报告整合思路
Web UI 测试是我接触 Pytest 最多的方向之一,几乎每个团队都在用 Page Object 模式(PO 模式)。PO 模式的核心是:把页面元素定位和页面操作封装成独立的类,测试用例只关注业务操作和断言,不再关心 XPath 和 CSS。
一个简单的 PO 类长这样:
class LoginPage: def __init__(self, driver): self.driver = driver def input_username(self, username): self.driver.find_element(By.ID, "username").send_keys(username) def input_password(self, password): self.driver.find_element(By.ID, "password").send_keys(password) def click_login(self): self.driver.find_element(By.ID, "login_btn").click()配合 Pytest 的 fixture,可以写一个返回页面对象的场景:
@pytest.fixture def login_page(driver): return LoginPage(driver) def test_login_success(login_page): login_page.input_username("admin") login_page.input_password("123456") login_page.click_login() assert login_page.driver.current_url == "/dashboard"至于 Allure,它并不仅是一个报告工具,更是一套"结构化展示"机制。在用例上打@allure.title、@allure.description或@allure.step,报告里就能看到清晰的步骤链路。结合上文的参数化,可以直接生成"每个参数组合对应一条 Allure 用例"的可视化报告,整个测试过程错在哪一步、哪个数据导致的问题,一眼便知。
5. 实操中的疑难杂症与排查技巧实录
5.1 环境搭建问题:Python 安装到 Pytest 安装的一揽子方案
很多新手卡在第一步,也就是环境和工具链的配置。针对最常见的场景,我给一份亲测可行的方案。
先装 Python。到 Python 官网下载对应系统的安装包,安装时务必勾选"Add Python to PATH"。装完打开终端执行python --version能正确输出版本号,就说明 PATH 配置成功。如果命令提示找不到 python,多半是 PATH 勾选漏了,可以手动把 Python 安装目录加到系统环境变量里,然后重启终端。
接着用 pip 安装 Pytest:
pip install pytest如果你用的是国内网络环境,可以临时换源加速:
pip install pytest -i https://pypi.tuna.tsinghua.edu.cn/simple装完验证一下版本:
pytest --version如果pytest命令提示找不到,而你在 venv 环境下,先确认当前虚拟环境已经被激活。常见错误是把 Pytest 装进了全局环境,但 IDE 里选择的解释器却是虚拟环境,导致怎么都 import 不到 pytest。这个问题的排查思路非常明确:在 PyCharm 或 VS Code 里设置正确的 Python 解释器路径,然后重新打开终端激活环境。
VS Code 用户还需要装 Python 扩展,之后可以在settings.json里配置 Pytest 路径和参数。PyCharm 用户在 Settings 里找到 "Tools -> Python Integrated Tools -> Testing",把默认测试运行器切换成 pytest 即可。
5.2 测试隔离问题:fixture 状态污染
状态污染是 Pytest 项目里最隐蔽也最容易回踩的问题。我举一个真实踩坑案例:某个 session 级的 fixture 返回一个列表,第一个用例往列表里加了一个元素,后面的用例读到的数据就多了一项,断言从第二条开始全部挂掉。
排查过程并不复杂,把所有 fixture 的scope列一张表,逐个确认共享范围,重点排查有没有"被修改的共享对象"。治本方案有两条:
- 把 fixture 降级为
scope="module"或scope="function",牺牲少量性能换取隔离性。 - 在 fixture 的 yield 清理阶段做状态重置,比如列表清空、数据库事务回滚。
第二个方案我实际用下来更舒服,毕竟 session 级 fixture 的性能红利还是很值得保留的。只要在yield之后把可变对象恢复到初始状态,就能兼顾速度和稳定。
5.3 插件冲突与版本依赖问题
插件多了之后,第一个容易踩的坑是钩子函数互相覆盖。比如pytest-html和allure-pytest同时安装并且都重写了报告相关钩子时,测试报告可能只能输出其中一种格式,甚至在收集阶段直接报错。解决方案是:明确自己的报告需求,按需安装,不要图省事把所有插件都装一遍。
第二个坑是插件版本和 Pytest 版本不兼容。比如某些旧版插件在 Pytest 8.x 下会报AttributeError,原因往往是插件内部调用了被新版本移除的 API。遇到这类问题,直接去 GitHub 看插件的 Release Notes 和兼容性说明,比自己在代码里瞎调试要高效得多。
我维护过一份项目依赖清单,每次升级 Pytest 主版本时,先把清单里所有插件一并升级到最新稳定版,再跑一遍完整的测试集。如果个别插件实在不兼容,就在requirements.txt里固定它的版本,单独为它保留旧环境。
5.4 常见问题速查表
| 问题现象 | 常见原因 | 解决建议 |
|---|---|---|
| pytest: command not found | Python 没有加入 PATH,或虚拟环境未激活 | 激活虚拟环境,检查 PATH 配置 |
| 中文 ids 显示为转义字符 | 参数化未指定 ids | 显式传 ids,或自定义编码 |
| 用例数量过多导致运行慢 | 参数化组合爆炸 | 裁剪组合,或使用 xdist 并行 |
| fixture 返回值被后续用例修改 | 共享对象状态污染 | 降级 scope 或 yield 后重置 |
| 安装了插件但没生效 | 版本不兼容或需要重启 IDE | 检查版本,重启 IDE |
| 测试报告只有部分用例 | xdist 并行与报告插件冲突 | 合并 XML 结果或串行执行 |
| 用例执行顺序不符合预期 | 默认按文件名字典序 | 用 pytest-ordering 指定顺序 |
| pytest.raises 捕获不到异常 | 被测代码捕获了异常未抛出 | 检查代码逻辑,确认异常确实外抛 |
5.5 一个完整的动态参数化 + 插件运行示例
最后放一个完整的可运行示例,把本文第 2 节到第 4 节的内容串起来。假设我们要测试一个用户登录接口,测试数据存放在login_cases.csv中,同时需要生成 HTML 报告并并行加速。目录结构如下:
tests/ conftest.py test_login.py login_cases.csvconftest.py内容:
import pytest def pytest_addoption(parser): parser.addoption("--env", action="store", default="test", help="测试环境")test_login.py内容:
import csv import pytest def load_login_cases(): with open("login_cases.csv", encoding="utf-8") as f: rows = list(csv.DictReader(f)) return [(row["username"], row["password"], row["expected"]) for row in rows] @pytest.mark.parametrize("username, password, expected", load_login_cases(), ids=[f"u{i}" for i in range(len(load_login_cases()))]) def test_login(username, password, expected): # 这里替换为你自己的接口调用或 UI 操作 result = perform_login(username, password) assert result.status == expectedlogin_cases.csv内容:
username,password,expected admin,123456,success user1,wrong_pwd,fail guest,guest123,success执行命令:
pytest tests/test_login.py -n auto --html=report.html --self-contained-html -v用-n auto让 xdist 自动分配进程,--html=report.html --self-contained-html生成单文件 HTML 报告,方便直接分享给团队成员。
这个示例虽然简单,但足够说明一条完整链路:用约定目录组织测试,用 fixture 准备环境,用参数化驱动数据,用插件完成并行和报告输出。把这套组合熟练之后,你会发现 Pytest 就像一个积木盒,基础功能和扩展插件各有各的位置,拼在一起就是你自己最顺手的测试工程。
根据我个人经验,学 Pytest 最快的方式不是从头读文档,而是拿一个手头的小项目直接改造。先把 unittest 写的那几个用例迁过来,然后找一个重复数据最多的用例改成参数化,最后找一个经常手动操作的任务试试用插件自动化。每一步都能立刻看到收益,你自然就会越用越深。祝你在 Pytest 这条路上少踩坑、多沉淀。