# 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 前) 1. **无模板相关测试** — 现有集成测试仅在 `TC-LM-003` 中使用临时创建的 fake 模板验证 styles.xml 存在,未测试真实的 BallList/BallMAP 模板分离 2. **无样式正确性验证** — 现有测试仅验证 `styles.xml` 文件存在,未验证字体/边框/填充/对齐/列宽/行高的实际内容 3. **无模板降级测试** — 未测试模板不存在/解析失败时的优雅降级行为 4. **无两方向独立模板测试** — 未验证 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 测试原则 1. **单元测试** (`test_pinmap.py`):纯逻辑测试,不读/写磁盘文件,使用内存中的 cell dict 2. **集成测试** (`run_tests.py`):文件级测试,使用 `Test/fixtures/` 中的真实 .xlsx 文件和临时目录中的动态创建文件 3. **模板 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 关键验证点 1. **MAP→List 不使用 BallMAP 模板** — 即使 BallMAP-Template.xlsx 存在,MAP→List 也不会加载它 2. **List→MAP 不使用 BallList 模板** — 即使 BallList-Template.xlsx 存在,List→MAP 也不会加载它 3. **不加载旧 PinMAP-Template.xlsx** — 即使旧模板存在,两个方向都不加载 ### 5.3 单元测试:新增到 `test_pinmap.py` 以下测试**可**新增到 `test_pinmap.py`(纯逻辑,不依赖文件系统): #### U-F009-F010-001: `_find_balllist_template_path` 和 `_find_ballmap_template_path` 路径生成 ```python 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: 无模板时使用默认样式 ```python 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: 有模板时使用模板字体 ```python 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: 有模板时使用模板边框 ```python 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: 有模板时使用模板填充 ```python 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 决定(不复制模板行列结构) ```python 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` 创建带特定格式的模板文件: ```python # 创建 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 下一步 1. **test-executor** 按本方案执行 Fixture 准备(创建 5 个模板文件) 2. **test-executor** 按优先级 P0 → P1 → P2 实施测试代码 3. 运行完整测试套件验证 4. 生成 v1.5.0 最终测试报告 --- *测试方案结束 — 等待 test-executor 执行*