1
0

test: add comprehensive pytest test suite

Add complete test infrastructure for yaml2pptx project with 245+ tests
covering unit, integration, and end-to-end scenarios.

Test structure:
- Unit tests: elements, template system, validators, loaders, utils
- Integration tests: presentation and rendering flows
- E2E tests: CLI commands (convert, check, preview)

Key features:
- PptxFileValidator for Level 2 PPTX validation (file structure,
  element count, content matching, position tolerance)
- Comprehensive fixtures for test data consistency
- Mock-based testing for external dependencies
- Test images generated with PIL/Pillow
- Boundary case coverage for edge scenarios

Dependencies added:
- pytest, pytest-cov, pytest-mock
- pillow (for test image generation)

Documentation updated:
- README.md: test running instructions
- README_DEV.md: test development guide

Co-authored-by: OpenSpec change: add-comprehensive-tests
This commit is contained in:
2026-03-02 23:11:34 +08:00
parent 027a832c9a
commit ab2510a400
56 changed files with 7035 additions and 6 deletions

456
tests/unit/test_template.py Normal file
View File

@@ -0,0 +1,456 @@
"""
模板系统单元测试
测试 Template 类的初始化、变量解析、条件渲染和模板渲染功能
"""
import pytest
from pathlib import Path
from loaders.yaml_loader import YAMLError
from core.template import Template
# ============= 模板初始化测试 =============
class TestTemplateInit:
"""Template 初始化测试类"""
def test_init_without_template_dir_raises_error(self, temp_dir):
"""测试未指定模板目录会引发错误"""
with pytest.raises(YAMLError, match="未指定模板目录"):
Template("title-slide", templates_dir=None)
def test_init_with_path_separator_in_name_raises_error(self, temp_dir):
"""测试模板名称包含路径分隔符会引发错误"""
with pytest.raises(YAMLError, match="模板名称不能包含路径分隔符"):
Template("../etc/passwd", templates_dir=temp_dir)
def test_init_with_nonexistent_template_raises_error(self, temp_dir):
"""测试模板文件不存在会引发错误"""
with pytest.raises(YAMLError, match="模板文件不存在"):
Template("nonexistent", templates_dir=temp_dir)
def test_init_with_valid_template(self, sample_template):
"""测试使用有效模板初始化"""
template = Template("title-slide", templates_dir=sample_template)
assert template.data is not None
assert "elements" in template.data
assert "vars" in template.data
# ============= 变量解析测试 =============
class TestResolveValue:
"""resolve_value 方法测试类"""
def test_resolve_value_simple_variable(self, sample_template):
"""测试解析简单变量"""
template = Template("title-slide", templates_dir=sample_template)
result = template.resolve_value("{title}", {"title": "My Title"})
assert result == "My Title"
def test_resolve_value_multiple_variables(self, sample_template):
"""测试解析多个变量"""
template = Template("title-slide", templates_dir=sample_template)
result = template.resolve_value("{title} - {subtitle}", {
"title": "Main",
"subtitle": "Sub"
})
assert result == "Main - Sub"
def test_resolve_value_undefined_variable_raises_error(self, sample_template):
"""测试未定义变量会引发错误"""
template = Template("title-slide", templates_dir=sample_template)
with pytest.raises(YAMLError, match="未定义的变量"):
template.resolve_value("{undefined}", {"title": "Test"})
def test_resolve_value_preserves_non_string(self):
"""测试非字符串值保持原样"""
template = Template("title-slide", templates_dir=sample_template)
assert template.resolve_value(123, {}) == 123
assert template.resolve_value(None, {}) is None
assert template.resolve_value(["list"], {}) == ["list"]
def test_resolve_value_converts_to_integer(self, sample_template):
"""测试结果转换为整数"""
template = Template("title-slide", templates_dir=sample_template)
result = template.resolve_value("{value}", {"value": "42"})
assert result == 42
assert isinstance(result, int)
def test_resolve_value_converts_to_float(self, sample_template):
"""测试结果转换为浮点数"""
template = Template("title-slide", templates_dir=sample_template)
result = template.resolve_value("{value}", {"value": "3.14"})
assert result == 3.14
assert isinstance(result, float)
# ============= resolve_element 测试 =============
class TestResolveElement:
"""resolve_element 方法测试类"""
def test_resolve_element_dict(self, sample_template):
"""测试解析字典元素"""
template = Template("title-slide", templates_dir=sample_template)
elem = {"content": "{title}", "box": [1, 2, 3, 4]}
result = template.resolve_element(elem, {"title": "Test"})
assert result["content"] == "Test"
assert result["box"] == [1, 2, 3, 4]
def test_resolve_element_list(self, sample_template):
"""测试解析列表元素"""
template = Template("title-slide", templates_dir=sample_template)
elem = ["{a}", "{b}", "static"]
result = template.resolve_element(elem, {"a": "A", "b": "B"})
assert result == ["A", "B", "static"]
def test_resolve_element_nested_structure(self, sample_template):
"""测试解析嵌套结构"""
template = Template("title-slide", templates_dir=sample_template)
elem = {
"font": {
"size": "{size}",
"color": "#000000"
}
}
result = template.resolve_element(elem, {"size": "24"})
assert result["font"]["size"] == 24
assert result["font"]["color"] == "#000000"
def test_resolve_element_excludes_visible(self, sample_template):
"""测试 visible 字段被排除"""
template = Template("title-slide", templates_dir=sample_template)
elem = {"content": "test", "visible": "{condition}"}
result = template.resolve_element(elem, {})
assert "visible" not in result
# ============= 条件渲染测试 =============
class TestEvaluateCondition:
"""evaluate_condition 方法测试类"""
def test_evaluate_condition_with_non_empty_variable(self, sample_template):
"""测试非空变量条件为真"""
template = Template("title-slide", templates_dir=sample_template)
result = template.evaluate_condition("{subtitle != ''}", {
"subtitle": "Test Subtitle"
})
assert result is True
def test_evaluate_condition_with_empty_variable(self, sample_template):
"""测试空变量条件为假"""
template = Template("title-slide", templates_dir=sample_template)
result = template.evaluate_condition("{subtitle != ''}", {
"subtitle": ""
})
assert result is False
def test_evaluate_condition_with_missing_variable(self, sample_template):
"""测试缺失变量条件为假"""
template = Template("title-slide", templates_dir=sample_template)
result = template.evaluate_condition("{subtitle != ''}", {})
assert result is False
def test_evaluate_condition_unrecognized_format_returns_true(self, sample_template):
"""测试无法识别的条件格式默认返回 True"""
template = Template("title-slide", templates_dir=sample_template)
result = template.evaluate_condition("some other format", {})
assert result is True
# ============= 模板渲染测试 =============
class TestRender:
"""render 方法测试类"""
def test_render_with_required_variable(self, sample_template):
"""测试渲染包含必需变量的模板"""
template = Template("title-slide", templates_dir=sample_template)
result = template.render({"title": "My Presentation"})
assert len(result) == 2 # 两个元素
assert result[0]["content"] == "My Presentation"
def test_render_with_optional_variable(self, sample_template):
"""测试渲染包含可选变量的模板"""
template = Template("title-slide", templates_dir=sample_template)
result = template.render({"title": "Test", "subtitle": "Subtitle"})
assert len(result) == 2 # 两个元素
def test_render_without_optional_variable(self, sample_template):
"""测试不提供可选变量"""
template = Template("title-slide", templates_dir=sample_template)
result = template.render({"title": "Test"})
# subtitle 元素应该被跳过visible 条件)
assert len(result) == 1
def test_render_missing_required_variable_raises_error(self, sample_template):
"""测试缺少必需变量会引发错误"""
template = Template("title-slide", templates_dir=sample_template)
with pytest.raises(YAMLError, match="缺少必需变量"):
template.render({}) # 缺少 title
def test_render_with_default_value(self, temp_dir):
"""测试使用默认值"""
# 创建带默认值的模板
template_content = """
vars:
- name: title
required: true
- name: subtitle
required: false
default: "Default Subtitle"
elements:
- type: text
content: "{title}"
- type: text
content: "{subtitle}"
"""
template_file = temp_dir / "templates" / "default-test.yaml"
template_file.parent.mkdir(exist_ok=True)
template_file.write_text(template_content)
template = Template("default-test", templates_dir=temp_dir / "templates")
result = template.render({"title": "Test"})
assert len(result) == 2
def test_render_filters_elements_by_visible_condition(self, sample_template):
"""测试根据 visible 条件过滤元素"""
template = Template("title-slide", templates_dir=sample_template)
# 有 subtitle - 应该显示两个元素
result_with = template.render({"title": "Test", "subtitle": "Sub"})
assert len(result_with) == 2
# 无 subtitle - 应该只显示一个元素
result_without = template.render({"title": "Test"})
assert len(result_without) == 1
# ============= 边界情况补充测试 =============
class TestTemplateBoundaryCases:
"""模板系统边界情况测试"""
def test_nested_variable_resolution(self, temp_dir):
"""测试嵌套变量解析"""
template_content = """
vars:
- name: title
required: true
- name: full_title
required: false
default: "{title} - Extended"
elements:
- type: text
content: "{full_title}"
box: [0, 0, 1, 1]
font: {}
"""
template_file = temp_dir / "templates" / "nested.yaml"
template_file.parent.mkdir(exist_ok=True)
template_file.write_text(template_content)
template = Template("nested", templates_dir=temp_dir / "templates")
result = template.render({"title": "Main"})
# 默认值中的变量应该被解析
assert result[0]["content"] == "Main - Extended"
def test_variable_with_special_characters(self, temp_dir):
"""测试变量包含特殊字符"""
template_content = """
vars:
- name: title
required: true
elements:
- type: text
content: "{title}"
box: [0, 0, 1, 1]
font: {}
"""
template_file = temp_dir / "templates" / "special.yaml"
template_file.parent.mkdir(exist_ok=True)
template_file.write_text(template_content)
template = Template("special", templates_dir=temp_dir / "templates")
special_values = [
"Test & Data",
"Test <Script>",
"Test \"Quotes\"",
"Test 'Apostrophe'",
"测试中文",
"Test: colon",
"Test; semi"
]
for value in special_values:
result = template.render({"title": value})
assert result[0]["content"] == value
def test_variable_with_numeric_value(self, temp_dir):
"""测试数字变量值"""
template_content = """
vars:
- name: width
required: true
elements:
- type: text
content: "Width: {width}"
box: [0, 0, 1, 1]
font: {}
"""
template_file = temp_dir / "templates" / "numeric.yaml"
template_file.parent.mkdir(exist_ok=True)
template_file.write_text(template_content)
template = Template("numeric", templates_dir=temp_dir / "templates")
result = template.render({"width": "100"})
# 应该保持为字符串(因为是在 content 中)
assert result[0]["content"] == "Width: 100"
def test_empty_vars_list(self, temp_dir):
"""测试空的 vars 列表"""
template_content = """
vars: []
elements:
- type: text
content: "Static Content"
box: [0, 0, 1, 1]
font: {}
"""
template_file = temp_dir / "templates" / "novars.yaml"
template_file.parent.mkdir(exist_ok=True)
template_file.write_text(template_content)
template = Template("novars", templates_dir=temp_dir / "templates")
result = template.render({})
assert len(result) == 1
def test_multiple_visible_conditions(self, temp_dir):
"""测试多个可见性条件"""
template_content = """
vars:
- name: title
required: true
- name: subtitle
required: false
default: ""
- name: footer
required: false
default: ""
elements:
- type: text
content: "{title}"
box: [0, 0, 1, 1]
font: {}
- type: text
content: "{subtitle}"
visible: "{subtitle != ''}"
box: [0, 1, 1, 1]
font: {}
- type: text
content: "{footer}"
visible: "{footer != ''}"
box: [0, 2, 1, 1]
font: {}
"""
template_file = temp_dir / "templates" / "multi-condition.yaml"
template_file.parent.mkdir(exist_ok=True)
template_file.write_text(template_content)
template = Template("multi-condition", templates_dir=temp_dir / "templates")
# 只有 title
result1 = template.render({"title": "Test"})
assert len(result1) == 1
# 有 subtitle 和 footer
result2 = template.render({
"title": "Test",
"subtitle": "Sub",
"footer": "Foot"
})
assert len(result2) == 3
def test_variable_in_position(self, temp_dir):
"""测试变量在位置中使用"""
template_content = """
vars:
- name: x_pos
required: true
elements:
- type: text
content: "Test"
box: ["{x_pos}", 1, 1, 1]
font: {}
"""
template_file = temp_dir / "templates" / "pos-var.yaml"
template_file.parent.mkdir(exist_ok=True)
template_file.write_text(template_content)
template = Template("pos-var", templates_dir=temp_dir / "templates")
result = template.render({"x_pos": "2"})
# 变量应该被解析为数字
assert result[0]["box"][0] == 2
def test_empty_template_elements(self, temp_dir):
"""测试空元素列表的模板"""
template_content = """
vars: []
elements: []
"""
template_file = temp_dir / "templates" / "empty.yaml"
template_file.parent.mkdir(exist_ok=True)
template_file.write_text(template_content)
template = Template("empty", templates_dir=temp_dir / "templates")
result = template.render({})
assert result == []
def test_variable_replacement_in_font(self, temp_dir):
"""测试字体属性中的变量替换"""
template_content = """
vars:
- name: font_size
required: true
- name: text_color
required: true
elements:
- type: text
content: "Styled Text"
box: [0, 0, 1, 1]
font:
size: {font_size}
color: {text_color}
bold: true
"""
template_file = temp_dir / "templates" / "font-vars.yaml"
template_file.parent.mkdir(exist_ok=True)
template_file.write_text(template_content)
template = Template("font-vars", templates_dir=temp_dir / "templates")
result = template.render({
"font_size": "24",
"text_color": "#ff0000"
})
assert result[0]["font"]["size"] == 24
assert result[0]["font"]["color"] == "#ff0000"