27 KiB
PinMAP-to-PinList v1.5.0 — 测试方案
版本: v1.5.0
日期: 2026-06-06
设计人: 测试架构师 (Test Architect)
基准: 修改需求评估docs/modification-assessment-v1.5.md
1. 变更影响范围
| 编号 | 标题 | 影响文件 | 测试重点 |
|---|---|---|---|
| F009 | MAP→List 使用 BallList-Template.xlsx | main.py |
模板分离后方向独立,互不干扰 |
| F010 | List→MAP 使用 BallMAP-Template.xlsx | main.py |
模板分离后方向独立,互不干扰 |
| F011 | 模板格式提取式应用 | xlsx_writer.py, template_reader.py |
字体/边框/填充/对齐/列宽/行高正确应用 |
| F012 | PinName 位置确认 + 回归测试 | test_pinmap.py |
上/下边 PinName 位置正确,往返一致性 |
2. 已有测试覆盖分析
2.1 单元测试 (Code/src/test_pinmap.py) — 9 个用例,全部通过
| 用例 | 覆盖范围 | v1.5 是否受影响 |
|---|---|---|
test_4x4_parse |
4×4 PinMAP 解析 (left/bottom/right/top) | ✅ 已验证 bottom/right Name 位置正确 |
test_4x4_validate |
4×4 PinMAP 验证 | 不受影响 |
test_missing_names_warning |
缺失 Name 警告 | 不受影响 |
test_duplicate_numbers |
重复序号错误 | 不受影响 |
test_gap_in_numbers |
序号不连续错误 | 不受影响 |
test_empty_cells |
空单元格异常 | 不受影响 |
test_no_pins |
无 Pin 数据异常 | 不受影响 |
test_12pin_square |
6×6 12Pin 解析和验证 | 不受影响 |
test_f012_pinname_position |
v1.5 新增 — 5×5 20Pin 往返一致性 | ✅ v1.5 核心回归测试 |
2.2 集成测试 (Test/run_tests.py) — 23 个用例,全部通过
| 编号 | 用例 | v1.5 是否受影响 |
|---|---|---|
| TC-MAP-001~006 | MAP→List 回归(含错误/警告场景) | ⚠️ F009 影响模板查找逻辑 |
| TC-LM-001~016 | List→MAP 新增(含错误/警告/往返) | ⚠️ F010/F011 影响模板样式应用 |
2.3 已有测试缺口(v1.5 前)
- 无模板相关测试 — 现有集成测试仅在
TC-LM-003中使用临时创建的 fake 模板验证 styles.xml 存在,未测试真实的 BallList/BallMAP 模板分离 - 无样式正确性验证 — 现有测试仅验证
styles.xml文件存在,未验证字体/边框/填充/对齐/列宽/行高的实际内容 - 无模板降级测试 — 未测试模板不存在/解析失败时的优雅降级行为
- 无两方向独立模板测试 — 未验证 MAP→List 和 List→MAP 使用不同模板时的隔离性
3. 完整测试方案
3.1 测试分层策略
| 层级 | 位置 | 测试什么 | 执行方式 |
|---|---|---|---|
| Unit | Code/src/test_pinmap.py |
纯逻辑测试(解析/验证/布局/往返),不依赖文件系统 | python3 Code/src/test_pinmap.py |
| Integration | Test/run_tests.py |
文件级测试(xlsx 读写/模板加载/端到端),依赖 fixtures/ 和临时文件 | python3 Test/run_tests.py |
3.2 测试原则
- 单元测试 (
test_pinmap.py):纯逻辑测试,不读/写磁盘文件,使用内存中的 cell dict - 集成测试 (
run_tests.py):文件级测试,使用Test/fixtures/中的真实 .xlsx 文件和临时目录中的动态创建文件 - 模板 fixture:需要准备
BallList-Template.xlsx和BallMAP-Template.xlsx两个模板 fixture 文件
4. F012 — PinName 位置回归测试
4.1 状态确认
代码当前行为:
pinmap_layout.py::get_name_cell: bottom →(r-1, c), top →(r+1, c)pinmap_parser.py: bottom name 读自(max_row-1, c), top name 读自(min_row+1, c)- 生成与解析使用相同约定,往返一致 ✅
F012 测试确认:当前代码已正确,测试已存在并全部通过。
4.2 已有回归测试(无需新增)
| 用例 | 位置 | 覆盖 |
|---|---|---|
test_f012_pinname_position |
test_pinmap.py |
5×5 20Pin 往返:生成 → 逐个验证四条边 Name 位置 → 解析回 PinList → 验证序号一致性 |
4.3 测试数据
test_pinmap.py 中的 4×4 数据已验证:
bottom: numbers at (6,2)=3, (6,3)=4; names at (5,2)=Pin3, (5,3)=Pin4 → max_row-1 ✅
top: numbers at (1,3)=7, (1,2)=8; names at (2,3)=Pin7, (2,2)=Pin8 → min_row+1 ✅
test_f012 5×5 数据已验证:
bottom: names at (4, c) = rows-1 = max_row-1 ✅
top: names at (2, c) = min_row+1 ✅
left: names at (r, 1) = c+1 ✅
right: names at (r, 4) = c-1 ✅
4.4 F012 测试结论
✅ 无需新增测试。test_f012_pinname_position 已充分覆盖 F012 需求,且当前测试全部通过。保持该测试持续运行即可。
5. F009 + F010 — 模板分离测试
5.1 测试场景矩阵
| 场景 | BallList-Template | BallMAP-Template | MAP→List 预期 | List→MAP 预期 |
|---|---|---|---|---|
| T-F009-01 | ✅ 存在 | — | 加载 BallList,应用其样式 | (不涉及) |
| T-F009-02 | ❌ 不存在 | — | 日志提示"未检测到",使用默认样式 | (不涉及) |
| T-F009-03 | 存在但损坏 | — | 日志提示"解析失败",使用默认样式 | (不涉及) |
| T-F010-01 | — | ✅ 存在 | (不涉及) | 加载 BallMAP,应用其样式 |
| T-F010-02 | — | ❌ 不存在 | (不涉及) | 日志提示"未检测到",使用默认样式 |
| T-F010-03 | — | 存在但损坏 | (不涉及) | 日志提示"解析失败",使用默认样式 |
| T-F009-04 | ✅ 存在 | ✅ 存在 | 加载 BallList | 加载 BallMAP(各自独立,互不干扰) |
5.2 关键验证点
- MAP→List 不使用 BallMAP 模板 — 即使 BallMAP-Template.xlsx 存在,MAP→List 也不会加载它
- List→MAP 不使用 BallList 模板 — 即使 BallList-Template.xlsx 存在,List→MAP 也不会加载它
- 不加载旧 PinMAP-Template.xlsx — 即使旧模板存在,两个方向都不加载
5.3 单元测试:新增到 test_pinmap.py
以下测试可新增到 test_pinmap.py(纯逻辑,不依赖文件系统):
U-F009-F010-001: _find_balllist_template_path 和 _find_ballmap_template_path 路径生成
def test_template_path_generation():
"""验证两个模板查找函数返回正确的路径格式。"""
import os
from main import _find_balllist_template_path, _find_ballmap_template_path
# 路径应以项目根目录为基础,包含正确的文件名
# 注意:这些函数检查文件是否存在,测试环境可能没有这些文件
# 所以只验证函数可调用且返回类型正确
result1 = _find_balllist_template_path()
result2 = _find_ballmap_template_path()
# 返回值要么是 str 要么是 None
assert result1 is None or isinstance(result1, str)
assert result2 is None or isinstance(result2, str)
# 两者应该是不同路径
if result1 and result2:
assert "BallList" in result1
assert "BallMAP" in result2
assert result1 != result2
print("✓ test_template_path_generation passed")
5.4 集成测试:新增到 Test/run_tests.py
以下测试需要 Test/fixtures/ 中的模板文件:
Fixture 准备:
| 文件 | 用途 | 内容要求 |
|---|---|---|
Test/fixtures/BallList-Template.xlsx |
MAP→List 模板 | 至少包含 fonts(Calibri 12pt, bold), borders(thin), column_widths |
Test/fixtures/BallMAP-Template.xlsx |
MAP→List 模板 | 至少包含 fonts(Arial 10pt), borders(medium), row_heights |
Test/fixtures/template_corrupt.xlsx |
损坏模板测试 | 一个非 xlsx 的普通文件伪装为 .xlsx |
新增集成测试用例:
TC-v1.5-001: MAP→List 加载 BallList 模板(模板存在)
前置: fixtures/BallList-Template.xlsx 存在
步骤:
1. 创建 PinMAP 输入(4×4)
2. 模拟 run_map_to_list 模板加载流程
3. 验证 read_template_styles 返回非 None
4. 验证返回的 TemplateStyle 包含字体信息
预期: 模板样式成功加载,输出文件包含 styles.xml
TC-v1.5-002: MAP→List 无模板降级(模板不存在)
前置: 删除根目录的 BallList-Template.xlsx
步骤:
1. 创建 PinMAP 输入(4×4)
2. 调用 _find_balllist_template_path()
3. 验证返回 None
预期: 返回 None,输出使用默认样式
TC-v1.5-003: List→MAP 加载 BallMAP 模板(模板存在)
前置: fixtures/BallMAP-Template.xlsx 存在
步骤:
1. 创建 5×5 PinList 输入(20 pin)
2. 模拟 run_list_to_map 模板加载流程
3. 验证 read_template_styles 返回非 None
4. 生成 PinMAP,验证输出包含 styles.xml
预期: 模板样式成功加载,输出文件包含 styles.xml
TC-v1.5-004: List→MAP 无模板降级(模板不存在)
前置: 删除根目录的 BallMAP-Template.xlsx
步骤:
1. 创建 5×5 PinList 输入(20 pin)
2. 调用 _find_ballmap_template_path()
3. 验证返回 None
预期: 返回 None,输出使用默认样式
TC-v1.5-005: 两个方向独立使用各自模板
前置: BallList-Template.xlsx 和 BallMAP-Template.xlsx 都存在
步骤:
1. MAP→List 方向:调用 _find_balllist_template_path(),验证路径包含 "BallList"
2. List→MAP 方向:调用 _find_ballmap_template_path(),验证路径包含 "BallMAP"
3. 验证两个路径不同
预期: 两个方向各自查找各自的模板文件,互不干扰
TC-v1.5-006: 模板损坏时优雅降级
前置: 提供一个损坏的 "template_corrupt.xlsx"
步骤:
1. 调用 read_template_styles("fixtures/template_corrupt.xlsx")
2. 验证返回 None(不抛异常)
预期: 返回 None,调用方可继续使用默认样式
6. F011 — 模板格式提取式应用测试
6.1 测试场景
F011 核心要求:模板的格式信息(字体/边框/填充/对齐/列宽/行高)正确应用到输出文件,但输出文件的行列结构由实际 Pin 数量决定。
6.2 单元测试:新增到 test_pinmap.py
F011 的样式构建逻辑在 xlsx_writer.py::StyledXLSXWriter 中,涉及 XML 字符串生成。以下测试可以在不写磁盘文件的情况下验证 XML 内容:
U-F011-001: 无模板时使用默认样式
def test_f011_default_styles_xml():
"""F011: 无模板时 _styles_xml() 返回硬编码默认样式。"""
from xlsx_writer import StyledXLSXWriter
writer = StyledXLSXWriter(style=None)
xml = writer._styles_xml()
# 验证硬编码默认值的存在
assert 'Calibri' in xml, "默认字体应为 Calibri"
assert 'thin' in xml or 'style="thin"' in xml, "默认边框应为 thin"
assert 'center' in xml, "默认对齐应为 center"
assert 'cellXfs count="4"' in xml, "应有 4 个 xf"
print("✓ test_f011_default_styles_xml passed")
U-F011-002: 有模板时使用模板字体
def test_f011_template_fonts_in_styles_xml():
"""F011: 有模板时 _styles_xml() 使用模板的字体信息(而非硬编码 Calibri)。"""
from template_reader import TemplateStyle, FontStyle
from xlsx_writer import StyledXLSXWriter
# 构建一个模板样式:微软雅黑 12pt + bold
style = TemplateStyle()
style.fonts = [
FontStyle(name="微软雅黑", size=12.0, bold=False, italic=False, color="FF000000"),
FontStyle(name="微软雅黑", size=12.0, bold=True, italic=False, color="FF000000"),
]
style.fills = []
style.borders = []
style.cell_xfs = [
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '0', 'xfId': '0'},
]
writer = StyledXLSXWriter(style=style)
xml = writer._styles_xml()
assert '微软雅黑' in xml, f"模板字体名应出现在 styles.xml 中\n{xml[:500]}"
assert '12' in xml or '12.0' in xml, f"模板字号 12pt 应出现在 styles.xml 中"
print("✓ test_f011_template_fonts_in_styles_xml passed")
U-F011-003: 有模板时使用模板边框
def test_f011_template_borders_in_styles_xml():
"""F011: 有模板时 _styles_xml() 使用模板的边框样式(而非硬编码 thin)。"""
from template_reader import TemplateStyle, BorderStyle, FontStyle
from xlsx_writer import StyledXLSXWriter
style = TemplateStyle()
style.fonts = [FontStyle(name="Calibri", size=11.0)]
style.borders = [
BorderStyle(top="none", bottom="none", left="none", right="none"),
BorderStyle(top="medium", bottom="medium", left="medium", right="medium"),
]
style.fills = []
style.cell_xfs = [
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '1', 'xfId': '0',
'applyBorder': '1'},
]
writer = StyledXLSXWriter(style=style)
xml = writer._styles_xml()
# 模板的 medium 边框应该存在(不仅仅是 thin)
assert 'medium' in xml, f"模板 medium 边框应出现在 styles.xml 中\n{xml[:800]}"
print("✓ test_f011_template_borders_in_styles_xml passed")
U-F011-004: 有模板时使用模板填充
def test_f011_template_fills_in_styles_xml():
"""F011: 有模板时 _styles_xml() 使用模板的填充色(而非硬编码 FFF0F0F0)。"""
from template_reader import TemplateStyle, FillStyle, FontStyle
from xlsx_writer import StyledXLSXWriter
style = TemplateStyle()
style.fonts = [FontStyle(name="Calibri", size=11.0)]
style.borders = []
style.fills = [
FillStyle(pattern_type="none", fg_color=""),
FillStyle(pattern_type="solid", fg_color="FFFF00"), # 黄色
]
style.cell_xfs = [
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '0', 'xfId': '0'},
{'numFmtId': '0', 'fontId': '0', 'fillId': '1', 'borderId': '0', 'xfId': '0',
'applyFill': '1'},
]
writer = StyledXLSXWriter(style=style)
xml = writer._styles_xml()
assert 'FFFF00' in xml, f"模板黄色填充应出现在 styles.xml 中\n{xml[:800]}"
print("✓ test_f011_template_fills_in_styles_xml passed")
U-F011-005: 输出行列由实际 Pin 决定(不复制模板行列结构)
def test_f011_output_dims_determined_by_pins():
"""F011: 输出文件的 dim 由实际 Pin 数量决定,不复制模板的行列结构。
即使模板有 100 行列定义,输出仍只包含实际 Pin 数据的行列。
"""
from template_reader import TemplateStyle, FontStyle
from xlsx_writer import StyledXLSXWriter
style = TemplateStyle()
style.fonts = [FontStyle(name="Calibri", size=11.0)]
style.column_widths = {i: 20.0 for i in range(100)} # 模板有 100 列
style.row_heights = {i: 30.0 for i in range(200)} # 模板有 200 行
style.fills = []
style.borders = []
style.cell_xfs = [
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '0', 'xfId': '0'},
]
# 仅输出 2 行 3 列的数据(模拟 2×2 PinMAP + A1)
data = {
'A1': 'QFP-8',
'A2': '1', 'B2': 'Pin1',
'A3': '2', 'B3': 'Pin2',
}
writer = StyledXLSXWriter(style=style)
sheet_xml = writer._sheet_xml(data)
# dim 应该反映实际数据范围(A1:C3),而非模板的 100 列
assert 'dimension ref="A1:C' in sheet_xml or 'dimension ref="A1:C3"' in sheet_xml, \
f"dim 应由实际数据决定,不应包含模板的 100 列\n{sheet_xml[:500]}"
# 不应出现 row r="201"(模板的第 200 行)
assert 'row r="201"' not in sheet_xml, "不应包含模板的多余行"
print("✓ test_f011_output_dims_determined_by_pins passed")
6.3 集成测试:新增到 Test/run_tests.py
TC-v1.5-007: 模板字体应用到输出文件
前置: fixtures/BallMAP-Template.xlsx 使用微软雅黑 14pt
步骤:
1. 用该模板生成 5×5 PinMAP 输出
2. 解压输出 xlsx,读取 xl/styles.xml
3. 验证 fonts 部分包含 "微软雅黑" 和 size=14
预期: 输出 styles.xml 包含模板字体定义
TC-v1.5-008: 模板列宽应用到输出文件
前置: fixtures/BallList-Template.xlsx 中 A 列宽=25, B 列宽=18
步骤:
1. 用该模板生成 PinList 输出
2. 解压输出 xlsx,读取 xl/worksheets/sheet1.xml
3. 验证 cols 元素中 A 列 width=25, B 列 width=18
预期: 输出列宽与模板一致
TC-v1.5-009: 模板行高应用到输出文件
前置: fixtures/BallMAP-Template.xlsx 中行高=25
步骤:
1. 用该模板生成 3×3 PinMAP 输出
2. 解压输出 xlsx,读取 xl/worksheets/sheet1.xml
3. 验证 row 元素包含 ht=25
预期: 输出行高与模板一致
TC-v1.5-010: 两个方向使用不同模板各自的格式
前置:
- BallList-Template.xlsx 字体=楷体 12pt
- BallMAP-Template.xlsx 字体=宋体 14pt
步骤:
1. MAP→List 方向:用 BallList 模板生成 PinList,验证输出字体=楷体 12pt
2. List→MAP 方向:用 BallMAP 模板生成 PinMAP,验证输出字体=宋体 14pt
3. 验证两个输出文件的字体不同
预期: 两个方向的输出各自使用对应模板的字体
7. F009/F010/F011 集成测试
7.1 端到端集成测试
TC-v1.5-011: 完整往返 + 模板隔离 (MAP→List→MAP)
前置: BallList-Template.xlsx 和 BallMAP-Template.xlsx 都存在且格式不同
步骤:
1. 使用 BallList 模板,执行 sample_4x4.xlsx → PinList
2. 验证 PinList 输出包含 BallList 模板的格式特征
3. 使用 BallMAP 模板,执行 PinList → PinMAP
4. 验证 PinMAP 输出包含 BallMAP 模板的格式特征
5. 验证往返后的 PinMAP 与原 PinMAP 数据一致(忽略格式差异)
预期:
- 往返数据完全一致(引脚序号/名称/封装信息)
- 中间 PinList 使用 BallList 模板格式
- 最终 PinMAP 使用 BallMAP 模板格式
TC-v1.5-012: 无模板完整流程
前置: 根目录没有 BallList-Template.xlsx 也没有 BallMAP-Template.xlsx
步骤:
1. 执行 MAP→List 转换
2. 执行 List→MAP 转换
3. 验证两个输出文件都能正常生成且数据正确
4. 验证两个输出文件使用默认样式(Calibri 11pt)
预期: 无模板情况下正常降级,输出使用默认样式,数据正确
8. 边界与异常测试
8.1 模板边界测试
| 编号 | 场景 | 预期行为 | 测试级别 |
|---|---|---|---|
| TC-BND-001 | 模板 fonts 为空列表 | 回退到默认字体 | Unit |
| TC-BND-002 | 模板 borders 为空列表 | 回退到默认边框(thin) | Unit |
| TC-BND-003 | 模板 fills 为空列表 | 回退到默认填充 | Unit |
| TC-BND-004 | 模板 cell_xfs 为空列表 | 回退到默认 cellXfs(4 个硬编码 xf) | Unit |
| TC-BND-005 | 模板 column_widths 为空 dict | 使用默认列宽 8.0 | Unit |
| TC-BND-006 | 模板 row_heights 为空 dict | 不使用自定义行高(使用 Excel 默认) | Unit |
| TC-BND-007 | 模板字体 color 缺少 FF 前缀 | xlsx_writer 自动补全 | Unit |
| TC-BND-008 | 模板填充 fg_color 缺少 FF 前缀 | xlsx_writer 自动补全 | Unit |
| TC-BND-009 | 模板中缺失 styles.xml | read_template_styles 返回 None | Unit |
| TC-BND-010 | 模板中缺失 sheet1.xml | column_widths/row_heights 为空 | Unit |
8.2 边界场景集成测试
TC-v1.5-013: 模板只有字体、无边框/填充
前置: 准备一个只有 fonts 定义的极简模板
步骤:
1. 加载模板样式
2. 验证 fonts 正确提取
3. 验证 borders/fills 使用默认值
4. 生成的输出文件可正常打开
预期: 字体来自模板,边框/填充使用默认值,文件可正常打开
TC-v1.5-014: 模板列宽少于输出列数
前置: 模板定义 3 列宽,但输出需要 6 列
步骤:
1. 加载模板(定义 A-C 列宽)
2. 生成 5×5(6 列) PinMAP
3. 验证模板定义的列使用模板宽度,额外的列使用默认 8.0
预期: 列宽正确扩展,无异常
9. 测试执行计划
9.1 新增单元测试(添加到 test_pinmap.py)
| 优先级 | 编号 | 测试名 | 简述 |
|---|---|---|---|
| P0 | U-F009-F010-001 | test_template_path_generation |
验证模板路径格式正确且互不相同 |
| P0 | U-F011-001 | test_f011_default_styles_xml |
无模板时使用默认样式 |
| P0 | U-F011-002 | test_f011_template_fonts_in_styles_xml |
模板字体应用到 styles.xml |
| P1 | U-F011-003 | test_f011_template_borders_in_styles_xml |
模板边框应用到 styles.xml |
| P1 | U-F011-004 | test_f011_template_fills_in_styles_xml |
模板填充应用到 styles.xml |
| P0 | U-F011-005 | test_f011_output_dims_determined_by_pins |
输出 dim 由 Pin 数决定 |
| P1 | U-BND-001 | test_template_empty_fonts_fallback |
空 fonts 回退 |
| P1 | U-BND-007 | test_template_color_prefix_auto_fix |
FF 前缀补全 |
| P1 | U-BND-009 | test_template_no_styles_xml |
缺失 styles.xml 降级 |
9.2 新增集成测试(添加到 Test/run_tests.py)
| 优先级 | 编号 | 测试名 | 简述 | 需要 Fixture |
|---|---|---|---|---|
| P0 | TC-v1.5-001 | MAP→List 加载 BallList 模板 | 模板存在时正确加载 | BallList-Template.xlsx |
| P0 | TC-v1.5-002 | MAP→List 无模板降级 | 模板不存在时优雅降级 | 无 |
| P0 | TC-v1.5-003 | List→MAP 加载 BallMAP 模板 | 模板存在时正确加载 | BallMAP-Template.xlsx |
| P0 | TC-v1.5-004 | List→MAP 无模板降级 | 模板不存在时优雅降级 | 无 |
| P0 | TC-v1.5-005 | 两个方向独立模板 | 各自使用各自的模板 | 两个模板 |
| P1 | TC-v1.5-006 | 模板损坏优雅降级 | 损坏模板不抛异常 | template_corrupt.xlsx |
| P1 | TC-v1.5-007 | 模板字体应用到输出 | 输出文件含模板字体 | 特殊字体模板 |
| P1 | TC-v1.5-008 | 模板列宽应用到输出 | 输出列宽=模板列宽 | 特殊列宽模板 |
| P1 | TC-v1.5-009 | 模板行高应用到输出 | 输出行高=模板行高 | 特殊行高模板 |
| P1 | TC-v1.5-010 | 两个方向各自格式 | 两个方向格式独立 | 两个不同格式模板 |
| P1 | TC-v1.5-011 | 完整往返+模板隔离 | MAP→List→MAP 数据一致 | 两个模板 |
| P1 | TC-v1.5-012 | 无模板完整流程 | 无模板正常降级 | 无 |
| P2 | TC-v1.5-013 | 极简模板 | 只有字体的模板 | 极简模板 |
| P2 | TC-v1.5-014 | 列宽扩展 | 模板列宽少于输出列数 | 窄模板 |
10. Fixture 准备清单
10.1 需要创建的 Fixture 文件
| 文件 | 放置位置 | 用途 | 关键内容 |
|---|---|---|---|
BallList-Template.xlsx |
Test/fixtures/ |
MAP→List 模板 | 字体=楷体 12pt, A列宽=25, B列宽=18, 边框=thin, 对齐=center |
BallMAP-Template.xlsx |
Test/fixtures/ |
List→MAP 模板 | 字体=宋体 14pt, 行高=25, 边框=medium, 填充=淡黄 FFFFFF00 |
template_corrupt.xlsx |
Test/fixtures/ |
损坏模板测试 | 一个无效的 ZIP 文件或文本文件伪装为 .xlsx |
template_minimal.xlsx |
Test/fixtures/ |
极简模板测试 | 只有 1 个 font 定义,无 borders/fills |
template_narrow.xlsx |
Test/fixtures/ |
列宽扩展测试 | 只定义 3 列宽(A-C),但测试输出 6 列 |
10.2 集成测试时的模板放置策略
集成测试需要能临时将模板放到正确位置。建议方案:
方案 A(推荐):在 run_tests.py 中使用 tempfile.mkdtemp 创建临时目录,将 fixture 模板复制为 BallList-Template.xlsx / BallMAP-Template.xlsx,然后通过 os.chdir 或修改 sys.path 让 main.py 的模板查找逻辑找到它们。
方案 B:在 run_tests.py 中直接调用底层函数(read_template_styles, _find_balllist_template_path),不经过 main.py 的路径查找,而是直接传入 fixture 路径。
推荐 方案 B(更简洁),因为已有测试也是直接调用底层函数。
10.3 Fixture 创建方法
使用 xlsx_writer.py::write_xlsx_with_style 创建带特定格式的模板文件:
# 创建 BallList-Template.xlsx
from xlsx_writer import StyledXLSXWriter
from template_reader import TemplateStyle, FontStyle, BorderStyle, FillStyle
style = TemplateStyle()
style.fonts = [
FontStyle(name="楷体", size=12.0, bold=False, color="FF000000"),
FontStyle(name="楷体", size=12.0, bold=True, color="FF000000"),
]
style.borders = [
BorderStyle(top="none", bottom="none", left="none", right="none"),
BorderStyle(top="thin", bottom="thin", left="thin", right="thin"),
]
style.fills = [FillStyle(pattern_type="none")]
style.cell_xfs = [
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '0', 'xfId': '0'},
{'numFmtId': '0', 'fontId': '0', 'fillId': '0', 'borderId': '1', 'xfId': '0',
'applyBorder': '1', 'hAlign': 'center', 'vAlign': 'center'},
]
style.column_widths = {0: 25.0, 1: 18.0}
style.row_heights = {}
# 创建 PinList 模板数据(2 行示例数据)
data = {'A1': '封装信息', 'A2': 'Pin1', 'B2': '1', 'A3': 'Pin2', 'B3': '2'}
writer = StyledXLSXWriter(style)
writer.write(data, "fixtures/BallList-Template.xlsx")
11. 预期工作量
| 阶段 | 内容 | 预估时间 |
|---|---|---|
| Fixture 准备 | 创建 5 个模板 fixture 文件 | 30 min |
| 单元测试 | 新增约 10 个单元测试到 test_pinmap.py |
1 hr |
| 集成测试 | 新增约 14 个集成测试到 run_tests.py |
1.5 hr |
| 回归验证 | 运行全部测试确认无回归 | 15 min |
| 文档更新 | 更新测试报告 | 10 min |
| 总计 | ~3.5 hr |
12. 测试通过标准
| 条件 | 要求 |
|---|---|
| 现有 9 个单元测试 | 全部通过 |
| 现有 23 个集成测试 | 全部通过 |
| 新增 ~10 个单元测试 | 全部通过 |
| 新增 ~14 个集成测试 | 全部通过 |
F012 往返测试 (test_f012_pinname_position) |
持续通过 |
| F009/F010 模板分离 | 两个方向使用各自模板,互不干扰 |
| F011 模板格式提取 | 字体/边框/填充/对齐/列宽/行高从模板正确应用 |
| 无模板场景 | 优雅降级使用默认样式,不抛异常 |
| 损坏模板场景 | 优雅降级为 None,不抛异常 |
13. 总结
13.1 测试设计决策
| 决策 | 说明 |
|---|---|
| F012 不再新增测试 | test_f012_pinname_position 已充分覆盖,代码行为已确认正确 |
| F009/F010 模板分离测试侧重降级 | 核心风险在于模板不存在/解析失败时的行为,而非正常路径 |
| F011 样式测试分两层 | 单元层验证 XML 生成,集成层验证最终文件内容 |
| 单元测试不依赖文件系统 | 使用内存中的 TemplateStyle 对象直接构造测试数据 |
| 集成测试使用 fixture 文件 | 由 test-executor 预先创建模板 fixture,再运行测试 |
13.2 测试覆盖矩阵
F012 F009 F010 F011 边界 集成
现有 test_pinmap.py ✅ — — — — —
现有 run_tests.py — — — — — ✅
新增 unit tests — ✅ ✅ ✅ ✅ —
新增 integration tests— ✅ ✅ ✅ ✅ ✅
13.3 下一步
- test-executor 按本方案执行 Fixture 准备(创建 5 个模板文件)
- test-executor 按优先级 P0 → P1 → P2 实施测试代码
- 运行完整测试套件验证
- 生成 v1.5.0 最终测试报告
测试方案结束 — 等待 test-executor 执行