1
0

fix: 修复 7 个失败测试和 1 个错误测试

- 修复 conftest_pptx.py 中元素类型检测:使用 has_text_frame 替代不存在的 MSO_SHAPE.TEXT_BOX
- 修复 test_presentation.py 中 3 个测试:使用对象属性访问替代字典访问
- 修复 unit/test_presentation.py 中路径比较问题
- 添加缺失的 mock_template_class fixture

测试通过率: 316/316 (100%)
代码覆盖率: 94%
This commit is contained in:
2026-03-03 01:25:36 +08:00
parent e82a6a945e
commit 22614d6f55
9 changed files with 284 additions and 44 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-02

View File

@@ -0,0 +1,111 @@
# 设计文档:修复失败的测试
## Context
当前项目的测试套件包含 316 个测试,其中 7 个失败1 个错误。主要问题分为四类:
1. **API 不匹配问题**:测试假设 `render_slide` 返回字典列表但实际实现返回元素对象列表dataclass 实例)
2. **python-pptx 枚举问题**`MSO_SHAPE.TEXT_BOX` 在当前 python-pptx 版本中不存在,导致元素类型检测失败
3. **Windows 路径问题**:字符串路径与 Path 对象混合比较导致断言失败
4. **Fixture 缺失**`mock_template_class` fixture 未定义
项目约束:
- 必须保持 100% 测试通过率
- 修复不能破坏现有功能
- 需要保持代码覆盖率 >= 94%
## Goals / Non-Goals
**Goals:**
- 修复所有 7 个失败测试和 1 个错误测试
- 确保修复后测试仍能有效验证功能正确性
- 改进测试辅助函数的兼容性
**Non-Goals:**
- 不修改生产代码的功能逻辑
- 不添加新功能
- 不降低代码覆盖率
## Decisions
### D1: 处理 API 不匹配render_slide 返回值)
**选项 A**: 修改 `render_slide` 方法,返回字典而非对象
- 优点:保持测试代码不变
- 缺点:需要修改 `pptx_renderer.py``html_renderer.py` 的调用方式
**选项 B**: 修改测试代码,使用对象属性访问
- 优点:保持生产代码不变,测试更符合实际使用场景
- 缺点:需要修改多个测试文件
**选择B** - 修改测试代码,因为元素对象提供了更好的类型安全和 IDE 支持
### D2: 处理 MSO_SHAPE.TEXT_BOX 枚举不存在
**选项 A**: 使用 `shape.has_text_frame` 属性检测文本框
- 优点:跨版本兼容
- 缺点:需要改变检测逻辑
**选项 B**: 检查 `shape.shape_type` 是否等于 `MSO_SHAPE.AUTO_SHAPE_TYPE` (枚举值 1)
- 优点:简单直接
- 缺点:可能不准确
**选项 C**: 改用 `isinstance(shape, pptx.shapes.text_frame.TextFrameProxy)` 或类似方法
- 优点:准确
- 缺点:需要了解内部实现
**选择A** - 使用 `has_text_frame` 属性,这是检测形状是否有文本框的标准方法
### D3: 处理 Windows 路径比较
**选项 A**: 在测试中使用 `str()` 转换 Path 对象
- 优点:简单
- 缺点:可能引入平台特定代码
**选项 B**: 在 `Presentation` 类中标准化路径存储
- 优点:一劳永逸
- 缺点:影响较大
**选择A** - 在测试中转换类型,因为这是测试代码问题,不是生产代码问题
### D4: 处理缺失的 Fixture
**选项 A**: 添加 `mock_template_class` fixture
- 优点:保持测试完整
- 缺点:需要了解 mock 的使用方式
**选项 B**: 删除该测试(如果功能已被其他测试覆盖)
- 优点:减少维护负担
- 缺点:可能降低覆盖度
**选择A** - 添加 fixture因为该测试验证的功能未被其他测试覆盖
## Risks / Trade-offs
| 风险 | 描述 | 缓解措施 |
|------|------|----------|
| 修复后功能回归 | 修改测试可能隐藏真正的 bug | 修复后运行完整测试套件,确认所有测试通过 |
| 版本兼容性 | 不同 python-pptx 版本可能有不同行为 | 使用跨版本兼容的 API`has_text_frame` |
| 测试脆弱性 | 测试与实现耦合过紧 | 使用公共 API减少内部实现细节依赖 |
## Migration Plan
1. 备份当前测试代码
2. 修改 `tests/conftest_pptx.py` 中的 `count_elements_by_type` 方法
3. 修改 `tests/integration/test_presentation.py` 中的测试
4. 修改 `tests/integration/test_rendering_flow.py` 中的测试
5. 修改 `tests/unit/test_presentation.py` 中的测试
6. 添加缺失的 fixture
7. 运行完整测试套件验证修复
8. 确保代码覆盖率不下降
## Open Questions
**Q1**: 是否需要为元素类型检测添加版本兼容性检查?
- 当前选择使用 `has_text_frame` 属性,兼容性较好
**Q2**: 是否应该将测试辅助函数移至独立的工具模块?
- 当前保持现状,避免引入不必要的重构

View File

@@ -0,0 +1,51 @@
# 修复失败的测试
## Why
当前项目有 7 个测试失败和 1 个测试错误,导致测试通过率从 100% 降至 97.5%。这些问题主要是由于:
1. 测试代码与实际 API 行为不一致
2. python-pptx 库版本变化导致的 API 差异
3. Windows 平台路径处理差异
4. 测试 fixture 定义缺失
如果不修复这些问题,将影响持续集成的可靠性,并阻碍后续功能开发。
## What Changes
### 测试修复
- 修复 `test_render_slide_with_template` - 调整测试以适应返回的元素对象
- 修复 `test_render_slide_with_variables` - 调整测试以适应返回的元素对象
- 修复 `test_render_direct_elements` - 调整测试以适应返回的元素对象
- 修复 `test_image_element_rendering` - 使用正确的枚举或替代方法检测文本框
- 修复 `test_shape_element_rendering` - 使用正确的枚举或替代方法检测形状
- 修复 `test_table_element_rendering` - 使用正确的枚举或替代方法检测表格
- 修复 `test_init_with_templates_dir` - 统一路径类型比较
- 修复 `test_render_slide_with_template_merges_background` - 添加缺失的 fixture
### 代码改进
-`tests/conftest_pptx.py` 中改进元素类型检测逻辑
- 添加更健壮的 python-pptx 版本兼容性处理
## Capabilities
### New Capabilities
- `test-fix-framework`: 建立测试修复的标准框架,确保未来 API 变更时测试能同步更新
### Modified Capabilities
- 无(现有功能的测试修复,不改变功能需求)
## Impact
### 受影响代码
- `tests/integration/test_presentation.py` - 3 个失败测试
- `tests/integration/test_rendering_flow.py` - 3 个失败测试
- `tests/unit/test_presentation.py` - 1 个失败测试 + 1 个错误
- `tests/conftest_pptx.py` - 辅助验证函数
### 测试影响
- 修复后测试通过率: 100% (316/316)
- 代码覆盖率: 维持 94%
### 依赖影响
- 无新增依赖
- 保持与 python-pptx 现有版本的兼容性

View File

@@ -0,0 +1,33 @@
# 测试修复规格说明
本变更是对现有测试代码的修复,不引入新功能,也不修改现有功能的行为。
## 不适用说明
此变更不涉及:
- 新功能需求
- 现有功能需求的修改
- 新增 API 或接口
因此,不需要创建新的规格说明文件。
所有需要修复的测试已经在现有测试套件中定义,修复的是测试代码与实现之间的不一致,而非功能需求的变化。
## 验证方式
修复完成后,运行以下命令验证:
```bash
# 运行所有测试
uv run pytest -v
# 验证测试通过率
uv run pytest --tb=short
# 检查代码覆盖率
uv run pytest --cov --cov-report=term-missing
```
预期结果:
- 所有 316 个测试通过
- 代码覆盖率 >= 94%

View File

@@ -0,0 +1,25 @@
# 任务清单:修复失败的测试
## 1. 修复 conftest_pptx.py 中的元素类型检测
- [x] 1.1 修复 `count_elements_by_type` 方法:将 `MSO_SHAPE.TEXT_BOX` 替换为使用 `shape.has_text_frame` 属性检测文本框
- [x] 1.2 运行修复后的测试验证 `test_image_element_rendering`
- [x] 1.3 运行修复后的测试验证 `test_shape_element_rendering`
- [x] 1.4 运行修复后的测试验证 `test_table_element_rendering`
## 2. 修复 integration/test_presentation.py 中的测试
- [x] 2.1 修复 `test_render_slide_with_template`:使用 `element.type``element.content` 属性访问,而非字典的 `get` 方法
- [x] 2.2 修复 `test_render_slide_with_variables`:同上
- [x] 2.3 修复 `test_render_direct_elements`:使用对象属性访问替换字典访问
## 3. 修复 unit/test_presentation.py 中的测试
- [x] 3.1 修复 `test_init_with_templates_dir`:将断言中的字符串转换为 Path 对象后比较
- [x] 3.2 添加缺失的 `mock_template_class` fixture 到 conftest.py
## 4. 验证和回归测试
- [x] 4.1 运行完整测试套件,确认所有 316 个测试通过
- [x] 4.2 运行代码覆盖率检查,确认覆盖率 >= 94%
- [x] 4.3 运行 e2e 测试验证整体功能未受影响

View File

@@ -14,6 +14,7 @@ sys.path.insert(0, str(project_root))
# ============= 基础 Fixtures =============
@pytest.fixture
def temp_dir(tmp_path):
"""临时目录 fixture使用 pytest 内置 tmp_path"""
@@ -50,19 +51,20 @@ slides:
def sample_yaml(temp_dir):
"""创建最小测试 YAML 文件"""
yaml_path = temp_dir / "test.yaml"
yaml_path.write_text(MINIMAL_YAML, encoding='utf-8')
yaml_path.write_text(MINIMAL_YAML, encoding="utf-8")
return yaml_path
# ============= 图片 Fixtures =============
@pytest.fixture
def sample_image(temp_dir):
"""创建测试图片文件(使用 Pillow 生成简单的 PNG"""
img_path = temp_dir / "test_image.png"
# 创建一个简单的红色图片
img = Image.new('RGB', (100, 100), color='red')
img.save(img_path, 'PNG')
img = Image.new("RGB", (100, 100), color="red")
img.save(img_path, "PNG")
return img_path
@@ -99,21 +101,33 @@ def sample_template(temp_dir):
"""创建测试模板目录和文件"""
template_dir = temp_dir / "templates"
template_dir.mkdir()
(template_dir / "title-slide.yaml").write_text(TEMPLATE_YAML, encoding='utf-8')
(template_dir / "title-slide.yaml").write_text(TEMPLATE_YAML, encoding="utf-8")
return template_dir
@pytest.fixture
def mock_template_class():
"""Mock Template 类,用于单元测试"""
from unittest.mock import patch
with patch("core.presentation.Template") as mock_class:
yield mock_class
# ============= PPTX 验证 Fixture =============
@pytest.fixture
def pptx_validator():
"""PPTX 文件验证器实例"""
from tests.conftest_pptx import PptxFileValidator
return PptxFileValidator()
# ============= 测试数据目录 Fixture =============
@pytest.fixture
def fixtures_dir():
"""测试数据目录路径"""
@@ -122,6 +136,7 @@ def fixtures_dir():
# ============= 额外的边界情况 Fixtures =============
@pytest.fixture
def edge_case_yaml_files(fixtures_dir):
"""所有边界情况 YAML 文件的路径"""

View File

@@ -117,9 +117,7 @@ class PptxFileValidator:
}
for shape in slide.shapes:
if shape.shape_type == MSO_SHAPE.TEXT_BOX:
counts["text_box"] += 1
elif hasattr(shape, "image"):
if hasattr(shape, "image"):
counts["picture"] += 1
elif shape.shape_type in [
MSO_SHAPE.RECTANGLE,
@@ -131,6 +129,8 @@ class PptxFileValidator:
counts["table"] += 1
elif shape.shape_type == MSO_SHAPE.GROUP:
counts["group"] += 1
elif hasattr(shape, "has_text_frame") and shape.has_text_frame:
counts["text_box"] += 1
else:
counts["other"] += 1

View File

@@ -87,9 +87,7 @@ slides:
# 模板变量应该被替换
elements = rendered["elements"]
title_elem = next(
e
for e in elements
if e.get("type") == "text" and "Test Title" in e.get("content", "")
e for e in elements if e.type == "text" and "Test Title" in e.content
)
assert title_elem is not None
@@ -157,8 +155,8 @@ slides:
# 检查变量是否被正确替换
elements = rendered["elements"]
assert any("My Title" in e.get("content", "") for e in elements)
assert any("My Subtitle" in e.get("content", "") for e in elements)
assert any("My Title" in e.content for e in elements)
assert any("My Subtitle" in e.content for e in elements)
class TestPresentationWithoutTemplate:
@@ -187,4 +185,4 @@ slides:
# 元素应该直接被渲染
assert len(rendered["elements"]) == 1
assert rendered["elements"][0]["content"] == "Direct Text"
assert rendered["elements"][0].content == "Direct Text"

View File

@@ -26,7 +26,7 @@ class TestPresentationInit:
"""测试带模板目录初始化"""
pres = Presentation(str(sample_yaml), str(sample_template))
assert pres.templates_dir == sample_template
assert pres.templates_dir == str(sample_template)
assert isinstance(pres.template_cache, dict)
def test_init_without_templates_dir(self, sample_yaml):
@@ -36,8 +36,8 @@ class TestPresentationInit:
assert pres.templates_dir is None
assert isinstance(pres.template_cache, dict)
@patch('core.presentation.load_yaml_file')
@patch('core.presentation.validate_presentation_yaml')
@patch("core.presentation.load_yaml_file")
@patch("core.presentation.validate_presentation_yaml")
def test_init_loads_yaml(self, mock_validate, mock_load, temp_dir):
"""测试初始化时加载 YAML"""
mock_data = {"slides": [{"elements": []}]}
@@ -121,7 +121,7 @@ slides:
class TestGetTemplate:
"""get_template 方法测试类"""
@patch('core.presentation.Template')
@patch("core.presentation.Template")
def test_get_template_caches_template(self, mock_template_class, sample_template):
"""测试模板被缓存"""
mock_template = Mock()
@@ -145,8 +145,10 @@ slides:
# 应该是同一个实例
assert template1 is template2
@patch('core.presentation.Template')
def test_get_template_creates_new_template(self, mock_template_class, sample_template):
@patch("core.presentation.Template")
def test_get_template_creates_new_template(
self, mock_template_class, sample_template
):
"""测试创建新模板"""
mock_template = Mock()
mock_template_class.return_value = mock_template
@@ -172,7 +174,7 @@ slides:
pres = Presentation(str(sample_yaml))
# 应该在调用 Template 时失败,而不是 get_template
with patch('core.presentation.Template') as mock_template_class:
with patch("core.presentation.Template") as mock_template_class:
mock_template_class.side_effect = YAMLError("No template dir")
with pytest.raises(YAMLError):
@@ -198,12 +200,19 @@ class TestRenderSlide:
assert len(result["elements"]) == 1
assert result["elements"][0].content == "Test"
@patch('core.presentation.Template')
def test_render_slide_with_template(self, mock_template_class, temp_dir, sample_template):
@patch("core.presentation.Template")
def test_render_slide_with_template(
self, mock_template_class, temp_dir, sample_template
):
"""测试渲染使用模板的幻灯片"""
mock_template = Mock()
mock_template.render.return_value = [
{"type": "text", "content": "Template Title", "box": [0, 0, 1, 1], "font": {}}
{
"type": "text",
"content": "Template Title",
"box": [0, 0, 1, 1],
"font": {},
}
]
mock_template_class.return_value = mock_template
@@ -216,10 +225,7 @@ slides:
pres = Presentation(str(yaml_path), str(sample_template))
slide_data = {
"template": "title-slide",
"vars": {"title": "My Title"}
}
slide_data = {"template": "title-slide", "vars": {"title": "My Title"}}
result = pres.render_slide(slide_data)
@@ -231,10 +237,7 @@ slides:
"""测试渲染带背景的幻灯片"""
pres = Presentation(str(sample_yaml))
slide_data = {
"background": {"color": "#ffffff"},
"elements": []
}
slide_data = {"background": {"color": "#ffffff"}, "elements": []}
result = pres.render_slide(slide_data)
@@ -244,16 +247,16 @@ slides:
"""测试渲染无背景的幻灯片"""
pres = Presentation(str(sample_yaml))
slide_data = {
"elements": []
}
slide_data = {"elements": []}
result = pres.render_slide(slide_data)
assert result["background"] is None
@patch('core.presentation.create_element')
def test_render_slide_converts_dict_to_objects(self, mock_create_element, sample_yaml):
@patch("core.presentation.create_element")
def test_render_slide_converts_dict_to_objects(
self, mock_create_element, sample_yaml
):
"""测试字典转换为元素对象"""
mock_elem = Mock()
mock_create_element.return_value = mock_elem
@@ -272,7 +275,9 @@ slides:
mock_create_element.assert_called()
assert result["elements"][0] == mock_elem
def test_render_slide_with_template_merges_background(self, mock_template_class, temp_dir, sample_template):
def test_render_slide_with_template_merges_background(
self, mock_template_class, temp_dir, sample_template
):
"""测试使用模板时合并背景"""
mock_template = Mock()
mock_template.render.return_value = [
@@ -292,7 +297,7 @@ slides:
slide_data = {
"template": "test",
"vars": {},
"background": {"color": "#ff0000"}
"background": {"color": "#ff0000"},
}
result = pres.render_slide(slide_data)
@@ -304,16 +309,16 @@ slides:
"""测试空元素列表"""
pres = Presentation(str(sample_yaml))
slide_data = {
"elements": []
}
slide_data = {"elements": []}
result = pres.render_slide(slide_data)
assert result["elements"] == []
@patch('core.presentation.create_element')
def test_render_slide_with_multiple_elements(self, mock_create_element, sample_yaml):
@patch("core.presentation.create_element")
def test_render_slide_with_multiple_elements(
self, mock_create_element, sample_yaml
):
"""测试多个元素"""
mock_elem1 = Mock()
mock_elem2 = Mock()
@@ -324,7 +329,7 @@ slides:
slide_data = {
"elements": [
{"type": "text", "content": "T1", "box": [0, 0, 1, 1], "font": {}},
{"type": "text", "content": "T2", "box": [1, 1, 1, 1], "font": {}}
{"type": "text", "content": "T2", "box": [1, 1, 1, 1], "font": {}},
]
}