567 pytest 单元测试 — 生信函数测试¶
适用人群:编程基础薄弱的生信方向毕业生 pytest 版本:9.0.3(2026 年 4 月发布,需 Python >= 3.10) pytest-cov 版本:7.1.0(2026 年 3 月发布) 关键词:pytest、单元测试、fixture、参数化、覆盖率
一、什么是单元测试?为什么生信代码更需要测试?¶
1.1 白话解释¶
单元测试 = 给你的代码买保险。
你写了一个计算 Shannon 多样性的函数,今天跑着没问题。过了两周你改了一行代码,结果 Shannon 的计算悄悄变了,你根本不知道——直到论文审稿人指出你的结果有问题。
单元测试就是:提前写好"标准答案",每次改代码后自动核对,一旦结果变了立刻告诉你。
1.2 生信代码为什么特别需要测试?¶
| 生信特殊性 | 举例 | 测试能帮什么 |
|---|---|---|
| 数据格式多 | FASTQ、SAM、VCF、TSV 每种格式都有坑 | 自动检查解析是否正确 |
| 边界条件多 | 空文件、只有一个物种、全是 N 碱基 | 用边界数据自动跑一遍 |
| 数值精度敏感 | Shannon 指数小数点后第 4 位不同就可能影响结论 | 用 pytest.approx() 精确比较 |
| 依赖外部工具 | fastp、kraken2 版本更新可能改变输出格式 | mock 掉外部依赖单独测试 |
| 结果难以肉眼验证 | 1000 个物种的丰度表,你不可能手动算 | 用已知答案的小数据集验证 |
二、pytest 安装与基本用法¶
2.1 安装¶
# 方式 1:conda 安装(推荐生信环境)
conda install -c conda-forge pytest # 安装 pytest 9.0.3
# 方式 2:pip 安装
pip install pytest # 自动安装最新版
# 验证安装
pytest --version # 应显示 pytest 9.0.3
2.2 最简用法¶
# 运行当前目录下所有测试
pytest # 自动查找 test_*.py 文件
# 运行指定文件
pytest tests/test_diversity.py # 只跑这个文件
# 运行指定函数
pytest tests/test_diversity.py::test_shannon # 只跑这一个测试
# 显示详细信息
pytest -v # verbose 模式,显示每个测试的结果
# 遇到第一个失败就停
pytest -x # 调试时很有用
# 显示 print 输出
pytest -s # 默认会捕获 stdout,加 -s 放出来
三、第一个测试:Shannon 多样性计算¶
3.1 先写函数¶
创建 diversity.py:
"""多样性指数计算模块"""
import numpy as np # 数值计算库
def shannon_diversity(abundances):
"""
计算 Shannon 多样性指数
参数:
abundances: 物种丰度列表(如 [0.3, 0.25, 0.2, 0.15, 0.1])
返回:
float: Shannon 指数 H = -sum(p * ln(p))
"""
abundances = np.array(abundances, dtype=float) # 转成 numpy 数组
if len(abundances) == 0: # 空列表
raise ValueError("丰度列表不能为空")
if np.any(abundances < 0): # 有负数
raise ValueError("丰度值不能为负")
total = np.sum(abundances) # 总和
if total == 0: # 全是 0
return 0.0
p = abundances[abundances > 0] / total # 计算相对比例(去掉 0)
return -np.sum(p * np.log(p)) # Shannon 公式
3.2 再写测试¶
创建 test_diversity.py:
"""Shannon 多样性计算的单元测试"""
import pytest # 导入 pytest
import numpy as np # 数值计算
from diversity import shannon_diversity # 导入被测函数
def test_shannon_equal_abundance():
"""等丰度分布:Shannon 应等于 ln(物种数)"""
# Arrange(准备)
abundances = [0.25, 0.25, 0.25, 0.25] # 4 个物种均匀分布
# Act(执行)
result = shannon_diversity(abundances) # 计算 Shannon
# Assert(断言)
expected = np.log(4) # 理论值 = ln(4) ≈ 1.3863
assert result == pytest.approx(expected, rel=1e-6) # 允许相对误差 1e-6
def test_shannon_single_species():
"""只有 1 个物种:Shannon 应为 0"""
result = shannon_diversity([1.0]) # 只有一个物种
assert result == pytest.approx(0.0) # 多样性为 0
def test_shannon_with_zeros():
"""包含 0 丰度的物种,应正确忽略"""
result = shannon_diversity([0.5, 0.5, 0, 0]) # 有两个 0
expected = np.log(2) # 只有 2 个有效物种
assert result == pytest.approx(expected, rel=1e-6)
def test_shannon_empty_raises():
"""空列表应抛出 ValueError"""
with pytest.raises(ValueError, match="不能为空"): # 期望抛错
shannon_diversity([]) # 传空列表
def test_shannon_negative_raises():
"""负数丰度应抛出 ValueError"""
with pytest.raises(ValueError, match="不能为负"):
shannon_diversity([0.5, -0.1, 0.6]) # 有负数
3.3 运行测试¶
输出示例:
test_diversity.py::test_shannon_equal_abundance PASSED [ 20%]
test_diversity.py::test_shannon_single_species PASSED [ 40%]
test_diversity.py::test_shannon_with_zeros PASSED [ 60%]
test_diversity.py::test_shannon_empty_raises PASSED [ 80%]
test_diversity.py::test_shannon_negative_raises PASSED [100%]
==================== 5 passed in 0.12s ====================
3.4 关键知识点¶
| 知识点 | 说明 |
|---|---|
assert | Python 内置断言,条件不满足就报错 |
pytest.approx() | 浮点数比较利器,避免 0.1+0.2 != 0.3 的问题 |
pytest.raises() | 测试"应该报错"的场景 |
match= | 配合 pytest.raises(),检查错误信息包含指定文字 |
四、测试组织¶
4.1 推荐目录结构¶
my_project/
├── src/ # 源代码
│ ├── diversity.py # 多样性计算
│ ├── abundance.py # 丰度表操作
│ └── parsers.py # 文件解析
├── tests/ # 测试代码
│ ├── __init__.py # 让 Python 识别为包(可为空)
│ ├── conftest.py # 共享的 fixture(后面讲)
│ ├── test_diversity.py # 测试多样性计算
│ ├── test_abundance.py # 测试丰度表操作
│ └── test_parsers.py # 测试文件解析
└── pytest.ini # pytest 配置(可选)
4.2 命名规范¶
| 项目 | 规则 | 示例 |
|---|---|---|
| 测试文件 | test_ 开头或 _test 结尾 | test_diversity.py |
| 测试函数 | test_ 开头 | test_shannon_equal_abundance() |
| 测试类 | Test 开头(可选) | class TestShannon: |
4.3 conftest.py — 共享 fixture¶
conftest.py 是 pytest 的"公共工具箱",放在 tests/ 目录下,里面的 fixture 所有测试文件都能用,不需要 import。
# tests/conftest.py
"""所有测试共享的 fixture"""
import pytest # 导入 pytest
import pandas as pd # 数据处理
import numpy as np # 数值计算
@pytest.fixture
def sample_abundance_df():
"""创建一个模拟的物种丰度 DataFrame"""
np.random.seed(42) # 固定随机种子(结果可重复)
data = pd.DataFrame(
np.random.dirichlet( # 生成随机丰度(总和为 1)
np.ones(10), size=5 # 10 个物种,5 个样本
).T,
index=[f"Species_{i}" for i in range(10)], # 物种名
columns=[f"Sample_{i}" for i in range(5)] # 样本名
)
return data
@pytest.fixture
def empty_abundance_df():
"""创建一个空的丰度表"""
return pd.DataFrame() # 空 DataFrame
五、Fixture 实战¶
5.1 什么是 Fixture?¶
白话:Fixture = 考试前发的草稿纸。 每次考试(测试)都给你一张新的,用完就扔,不会互相影响。
5.2 @pytest.fixture 装饰器¶
import pytest # 导入 pytest
import tempfile # 临时文件
import os # 文件操作
@pytest.fixture
def mock_fastq_content():
"""返回一段模拟的 FASTQ 内容"""
return (
"@SEQ_001\n" # 序列名
"ATCGATCGATCG\n" # 碱基序列
"+\n" # 分隔符
"IIIIIIIIIIII\n" # 质量分数(全是 I = Q40)
"@SEQ_002\n"
"GCTAGCTAGCTA\n"
"+\n"
"FFFFFFFFFFFF\n"
)
@pytest.fixture
def mock_fastq_file(tmp_path, mock_fastq_content):
"""创建一个临时 FASTQ 文件用于测试"""
# tmp_path 是 pytest 内置 fixture,自动创建临时目录
fq_file = tmp_path / "test.fastq" # 在临时目录创建文件
fq_file.write_text(mock_fastq_content) # 写入模拟内容
return str(fq_file) # 返回文件路径
def test_fastq_file_exists(mock_fastq_file):
"""测试 FASTQ 文件是否被正确创建"""
assert os.path.exists(mock_fastq_file) # 文件应该存在
def test_fastq_has_four_lines_per_read(mock_fastq_file):
"""FASTQ 每条 read 应该有 4 行"""
with open(mock_fastq_file) as f: # 打开文件
lines = f.readlines() # 读所有行
assert len(lines) % 4 == 0 # 行数应该是 4 的倍数
5.3 tmp_path — 测试文件 I/O 的利器¶
tmp_path 是 pytest 内置的 fixture,每次测试自动创建一个临时目录,测试结束后自动清理。
def test_write_abundance_csv(tmp_path, sample_abundance_df):
"""测试丰度表能否正确保存为 CSV"""
output_file = tmp_path / "abundance.csv" # 临时目录中的文件
sample_abundance_df.to_csv(output_file) # 保存 CSV
assert output_file.exists() # 文件应该被创建
assert output_file.stat().st_size > 0 # 文件不应为空
# 读回来验证内容
import pandas as pd
loaded = pd.read_csv(output_file, index_col=0) # 读取 CSV
assert loaded.shape == sample_abundance_df.shape # 行列数一致
六、参数化测试¶
6.1 @pytest.mark.parametrize — 一个测试跑多组数据¶
白话:同一张试卷,换不同的数字再考一遍。
import pytest # 导入 pytest
import numpy as np # 数值计算
from diversity import shannon_diversity # 导入被测函数
@pytest.mark.parametrize(
"abundances, expected_diversity", # 参数名
[
# (输入丰度, 期望的 Shannon 值)
([1.0], 0.0), # 1 个物种 → H=0
([0.5, 0.5], np.log(2)), # 2 个均匀 → H=ln(2)
([0.25, 0.25, 0.25, 0.25], np.log(4)), # 4 个均匀 → H=ln(4)
([0.1] * 10, np.log(10)), # 10 个均匀 → H=ln(10)
([0.9, 0.05, 0.05], None), # 不均匀 → 计算验证
],
ids=[ # 给每组测试起个名字
"single_species",
"two_equal",
"four_equal",
"ten_equal",
"uneven_distribution",
]
)
def test_shannon_parametrized(abundances, expected_diversity):
"""用多组数据测试 Shannon 多样性计算"""
result = shannon_diversity(abundances) # 计算实际值
if expected_diversity is not None: # 有期望值时比较
assert result == pytest.approx(expected_diversity, rel=1e-6)
else:
# 不均匀分布:只检查值在合理范围内
assert 0 < result < np.log(len(abundances)) # 0 < H < ln(S)
6.2 运行参数化测试¶
输出:
test_diversity.py::test_shannon_parametrized[single_species] PASSED
test_diversity.py::test_shannon_parametrized[two_equal] PASSED
test_diversity.py::test_shannon_parametrized[four_equal] PASSED
test_diversity.py::test_shannon_parametrized[ten_equal] PASSED
test_diversity.py::test_shannon_parametrized[uneven_distribution] PASSED
七、实战:为宏基因组分析函数写完整测试¶
7.1 被测模块 metagenomics.py¶
"""宏基因组分析常用函数"""
import pandas as pd # 数据处理
import numpy as np # 数值计算
from scipy.spatial.distance import braycurtis # Bray-Curtis 距离
def filter_low_abundance(df, min_mean=0.001):
"""
过滤低丰度物种
参数:
df: DataFrame,行=物种,列=样本
min_mean: 平均丰度阈值(默认 0.1%)
返回:
过滤后的 DataFrame
"""
if df.empty: # 空表直接返回
return df.copy()
mean_abund = df.mean(axis=1) # 每个物种的平均丰度
return df.loc[mean_abund >= min_mean].copy() # 只保留 >= 阈值的
def calculate_bray_curtis(df):
"""
计算样本间的 Bray-Curtis 距离矩阵
参数:
df: DataFrame,行=物种,列=样本
返回:
DataFrame: 样本 x 样本 的距离矩阵
"""
samples = df.columns.tolist() # 获取样本名列表
n = len(samples) # 样本数量
dist_matrix = np.zeros((n, n)) # 初始化距离矩阵
for i in range(n): # 双重循环
for j in range(i + 1, n): # 只算上三角
d = braycurtis( # scipy 的 Bray-Curtis
df.iloc[:, i].values, # 样本 i 的丰度向量
df.iloc[:, j].values # 样本 j 的丰度向量
)
dist_matrix[i, j] = d # 填上三角
dist_matrix[j, i] = d # 对称填下三角
return pd.DataFrame( # 转为 DataFrame
dist_matrix,
index=samples, # 行名 = 样本名
columns=samples # 列名 = 样本名
)
def parse_kraken_report(filepath):
"""
解析 Kraken2 报告文件
参数:
filepath: Kraken2 report 文件路径
返回:
list[dict]: 每行解析为一个字典
"""
results = [] # 存解析结果
with open(filepath) as f: # 打开文件
for line in f: # 逐行读
line = line.strip() # 去首尾空白
if not line: # 跳过空行
continue
parts = line.split("\t") # 按 tab 分割
if len(parts) < 6: # Kraken 报告至少 6 列
continue
results.append({
"percentage": float(parts[0]), # 第 1 列:百分比
"reads_clade": int(parts[1]), # 第 2 列:该分支的 reads 数
"reads_direct": int(parts[2]), # 第 3 列:直接命中的 reads 数
"rank": parts[3].strip(), # 第 4 列:分类等级(如 G=属)
"taxid": int(parts[4]), # 第 5 列:NCBI 分类 ID
"name": parts[5].strip() # 第 6 列:物种名
})
return results
7.2 完整测试文件 test_metagenomics.py¶
"""宏基因组分析函数的完整测试"""
import pytest # 测试框架
import pandas as pd # 数据处理
import numpy as np # 数值计算
from metagenomics import ( # 导入被测函数
filter_low_abundance,
calculate_bray_curtis,
parse_kraken_report
)
# ==================== Fixtures ====================
@pytest.fixture
def abundance_df():
"""标准丰度表 fixture"""
return pd.DataFrame(
{
"S1": [0.3, 0.25, 0.2, 0.001, 0.0005], # 样本 1
"S2": [0.1, 0.35, 0.15, 0.002, 0.0003], # 样本 2
"S3": [0.2, 0.2, 0.2, 0.003, 0.0001], # 样本 3
},
index=["Sp_A", "Sp_B", "Sp_C", "Sp_D", "Sp_E"] # 物种
)
@pytest.fixture
def identical_samples_df():
"""两个完全相同的样本"""
return pd.DataFrame(
{"S1": [0.5, 0.3, 0.2], "S2": [0.5, 0.3, 0.2]},
index=["Sp_A", "Sp_B", "Sp_C"]
)
@pytest.fixture
def kraken_report_file(tmp_path):
"""创建模拟的 Kraken2 报告文件"""
content = (
"25.50\t1000\t500\tG\t838\tBacteroides\n" # 属水平
"10.20\t400\t200\tG\t239935\tPrevotella\n" # 属水平
"5.00\t200\t100\tS\t817\tBacteroides fragilis\n" # 种水平
)
report_path = tmp_path / "kraken_report.txt" # 临时文件路径
report_path.write_text(content) # 写入内容
return str(report_path) # 返回路径字符串
# ==================== filter_low_abundance 测试 ====================
class TestFilterLowAbundance:
"""测试低丰度过滤功能"""
def test_filters_below_threshold(self, abundance_df):
"""低于阈值的物种应被过滤掉"""
result = filter_low_abundance(abundance_df, min_mean=0.01)
# Sp_D 平均 0.002,Sp_E 平均 0.0003 → 都应被过滤
assert "Sp_D" not in result.index # Sp_D 应该被去掉
assert "Sp_E" not in result.index # Sp_E 应该被去掉
assert "Sp_A" in result.index # Sp_A 应该保留
def test_keeps_above_threshold(self, abundance_df):
"""高于阈值的物种应保留"""
result = filter_low_abundance(abundance_df, min_mean=0.01)
assert len(result) == 3 # 应保留 3 个高丰度物种
def test_empty_dataframe(self):
"""空 DataFrame 应返回空 DataFrame"""
empty = pd.DataFrame()
result = filter_low_abundance(empty)
assert result.empty # 结果也是空的
def test_does_not_modify_original(self, abundance_df):
"""原始 DataFrame 不应被修改(不可变原则)"""
original_shape = abundance_df.shape # 记住原始形状
filter_low_abundance(abundance_df, min_mean=0.1) # 运行过滤
assert abundance_df.shape == original_shape # 原始数据形状不变
def test_zero_threshold_keeps_all(self, abundance_df):
"""阈值为 0 应保留所有物种"""
result = filter_low_abundance(abundance_df, min_mean=0)
assert len(result) == len(abundance_df) # 数量不变
# ==================== calculate_bray_curtis 测试 ====================
class TestCalculateBrayCurtis:
"""测试 Bray-Curtis 距离计算"""
def test_identical_samples_zero_distance(self, identical_samples_df):
"""完全相同的样本距离应为 0"""
result = calculate_bray_curtis(identical_samples_df)
assert result.loc["S1", "S2"] == pytest.approx(0.0) # 距离为 0
def test_distance_matrix_is_symmetric(self, abundance_df):
"""距离矩阵应该是对称的"""
result = calculate_bray_curtis(abundance_df)
# 矩阵转置后应与原矩阵相等
pd.testing.assert_frame_equal(result, result.T) # 对称性检查
def test_diagonal_is_zero(self, abundance_df):
"""对角线(自己与自己的距离)应为 0"""
result = calculate_bray_curtis(abundance_df)
for sample in abundance_df.columns:
assert result.loc[sample, sample] == pytest.approx(0.0)
def test_distance_between_0_and_1(self, abundance_df):
"""Bray-Curtis 距离范围应在 [0, 1]"""
result = calculate_bray_curtis(abundance_df)
assert (result.values >= -1e-10).all() # 不应有负值(允许微小浮点误差)
assert (result.values <= 1 + 1e-10).all() # 不应超过 1
def test_output_shape(self, abundance_df):
"""输出矩阵应是 n x n(n = 样本数)"""
result = calculate_bray_curtis(abundance_df)
n = len(abundance_df.columns)
assert result.shape == (n, n) # 3 个样本 → 3x3 矩阵
# ==================== parse_kraken_report 测试 ====================
class TestParseKrakenReport:
"""测试 Kraken2 报告解析"""
def test_parses_correct_count(self, kraken_report_file):
"""应解析出正确的行数"""
result = parse_kraken_report(kraken_report_file)
assert len(result) == 3 # 3 行数据
def test_parses_percentage(self, kraken_report_file):
"""百分比应正确解析为浮点数"""
result = parse_kraken_report(kraken_report_file)
assert result[0]["percentage"] == pytest.approx(25.50)
def test_parses_taxon_name(self, kraken_report_file):
"""物种名应正确解析"""
result = parse_kraken_report(kraken_report_file)
assert result[0]["name"] == "Bacteroides"
assert result[2]["name"] == "Bacteroides fragilis"
def test_parses_rank(self, kraken_report_file):
"""分类等级应正确解析"""
result = parse_kraken_report(kraken_report_file)
assert result[0]["rank"] == "G" # Genus(属)
assert result[2]["rank"] == "S" # Species(种)
def test_empty_file(self, tmp_path):
"""空文件应返回空列表"""
empty_file = tmp_path / "empty.txt"
empty_file.write_text("") # 创建空文件
result = parse_kraken_report(str(empty_file))
assert result == [] # 结果为空列表
def test_file_not_found_raises(self):
"""不存在的文件应抛出 FileNotFoundError"""
with pytest.raises(FileNotFoundError):
parse_kraken_report("/nonexistent/path.txt")
八、覆盖率:pytest-cov¶
8.1 安装¶
# conda 安装
conda install -c conda-forge pytest-cov # 安装 pytest-cov 7.1.0
# pip 安装
pip install pytest-cov # 自动安装最新版
8.2 使用¶
# 基本用法:测试 + 覆盖率报告
pytest --cov=src tests/ # --cov=源代码目录 测试目录
# 显示哪些行没被测到
pytest --cov=src --cov-report=term-missing # term-missing 显示漏掉的行号
# 生成 HTML 报告(推荐,可以在浏览器里看)
pytest --cov=src --cov-report=html # 生成 htmlcov/ 目录
# 设置最低覆盖率,低于 80% 就报失败
pytest --cov=src --cov-fail-under=80 # 覆盖率 < 80% → 退出码非 0
8.3 怎么看覆盖率报告¶
终端输出示例:
---------- coverage: platform linux, python 3.12.8 ----------
Name Stmts Miss Cover Missing
-----------------------------------------------------
src/diversity.py 12 0 100%
src/metagenomics.py 35 3 91% 42-44
src/parsers.py 28 8 71% 15-18, 25-28
-----------------------------------------------------
TOTAL 75 11 85%
| 列名 | 含义 |
|---|---|
| Stmts | 总语句数(可执行的代码行数) |
| Miss | 未被测试覆盖的行数 |
| Cover | 覆盖率百分比 |
| Missing | 具体哪些行没被测到 |
目标:整体覆盖率 >= 80%。 重点关注 Missing 列,补测那些没覆盖到的行。
8.4 配置文件(可选)¶
在 pyproject.toml 中统一配置 pytest 和覆盖率:
[tool.pytest] # pytest 9.x 原生 TOML 配置
testpaths = ["tests"] # 测试目录
addopts = "-v --cov=src --cov-report=term-missing" # 默认参数
[tool.coverage.run] # coverage 运行配置
source = ["src"] # 要统计的源代码目录
omit = ["tests/*", "*/migrations/*"] # 排除的路径
[tool.coverage.report] # 覆盖率报告配置
fail_under = 80 # 低于 80% 就失败
show_missing = true # 显示未覆盖行号
九、常见报错¶
报错 1:ModuleNotFoundError: No module named 'xxx'¶
原因: pytest 找不到你的源代码模块。 解决方案(选一个):
# 方案 A:在项目根目录用 pip 安装为可编辑包
pip install -e . # 需要 pyproject.toml 或 setup.py
# 方案 B:设置 PYTHONPATH
export PYTHONPATH="${PYTHONPATH}:$(pwd)/src" # 把 src 目录加到搜索路径
# 方案 C:在 conftest.py 里加路径
# tests/conftest.py
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
报错 2:AssertionError — 浮点数比较失败¶
原因: 浮点数精度问题,直接用 == 比较两个浮点数几乎一定会失败。 解决: 用 pytest.approx()。
# 错误:
assert shannon_diversity([0.5, 0.5]) == np.log(2)
# 正确:
assert shannon_diversity([0.5, 0.5]) == pytest.approx(np.log(2), rel=1e-6)
报错 3:fixture 'xxx' not found¶
原因: fixture 定义在别的文件里,但没放在 conftest.py 中。 解决: 把共享 fixture 移到 tests/conftest.py,pytest 会自动识别。
报错 4:collected 0 items¶
原因: pytest 没找到任何测试函数。 解决: 检查文件名是否以 test_ 开头,函数名是否以 test_ 开头。
# 错误:文件名 diversity_tests.py(不以 test_ 开头)
# 错误:函数名 check_shannon()(不以 test_ 开头)
# 正确:文件名 test_diversity.py
# 正确:函数名 test_shannon()
十、速查表¶
pytest 常用命令¶
pytest # 运行所有测试
pytest -v # 详细模式
pytest -x # 遇到失败立即停止
pytest -s # 显示 print 输出
pytest -k "shannon" # 只运行名字包含 shannon 的测试
pytest --lf # 只运行上次失败的测试
pytest --co # 只列出测试,不运行
pytest --cov=src # 带覆盖率
pytest --cov=src --cov-fail-under=80 # 覆盖率不到 80% 就失败
assert 断言速查¶
assert x == y # 相等
assert x != y # 不等
assert x > y # 大于
assert x in collection # 包含
assert isinstance(x, MyClass) # 类型检查
assert x == pytest.approx(y, rel=1e-6) # 浮点数近似相等
assert x == pytest.approx(y, abs=0.01) # 绝对误差 0.01
# 测试异常
with pytest.raises(ValueError): # 期望抛出 ValueError
risky_function()
with pytest.raises(ValueError, match="xxx"): # 期望错误信息包含 xxx
risky_function()
fixture 速查¶
@pytest.fixture # 基本 fixture
def my_data():
return [1, 2, 3]
@pytest.fixture(scope="module") # 模块级(整个文件共享一个)
def expensive_resource():
return load_big_data()
@pytest.fixture(autouse=True) # 自动应用(不需要传参)
def setup_logging():
logging.basicConfig(level=logging.DEBUG)
# 内置 fixture
# tmp_path → 临时目录(pathlib.Path 对象)
# tmp_path_factory → 创建多个临时目录
# capsys → 捕获 stdout/stderr
# monkeypatch → 临时修改环境变量、属性等
参数化速查¶
@pytest.mark.parametrize("input,expected", [
(arg1, result1),
(arg2, result2),
])
def test_xxx(input, expected):
assert func(input) == expected
关键概念一句话总结¶
| 概念 | 一句话 |
|---|---|
| 单元测试 | 代码的保险,改了代码自动检查有没有改坏 |
| pytest | Python 最流行的测试框架,简单好用 |
| assert | 断言,"我宣布这个条件必须成立" |
| fixture | 考试草稿纸,每次测试给你一份新的 |
| 参数化 | 同一张试卷换不同数字再考 |
| 覆盖率 | 你的代码有多少百分比被测试覆盖了 |
| TDD | 先写测试再写代码,红 → 绿 → 重构 |
| conftest.py | 公共工具箱,里面的 fixture 所有测试都能用 |
推荐阅读: 566_Python面向对象编程_生信实战.md — 给你的 class 写好之后,用 pytest 给它上保险