From 9be7f68a7a37f81aaa1c0043ff2ab6bff4e4a3ee Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 28 Jul 2026 13:45:54 +0800 Subject: [PATCH] =?UTF-8?q?test(wave157):=20domain=E5=B1=827=E4=B8=AA?= =?UTF-8?q?=E5=B0=8F=E6=A8=A1=E5=9D=97=E6=89=B9=E9=87=8F=E5=8D=95=E6=B5=8B?= =?UTF-8?q?=20+44?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 domain 层 7 个小模块新增 44 个单测,纯逻辑 0 外部依赖: - EditingMode 枚举:6个 - Tag 工厂:4个 - RecipeItem/Recipe:6个 - VoiceLibraryItem:3个 - TitleLibraryItem:3个 - TemplateSegment/Template/TemplateCategory:6个 - EditTemplateStatus/EditTemplate:16个 --- .../unit/domain/test_domain_small_modules.py | 421 ++++++++++++++++++ 1 file changed, 421 insertions(+) create mode 100644 tests/unit/domain/test_domain_small_modules.py diff --git a/tests/unit/domain/test_domain_small_modules.py b/tests/unit/domain/test_domain_small_modules.py new file mode 100644 index 000000000..55404f2f9 --- /dev/null +++ b/tests/unit/domain/test_domain_small_modules.py @@ -0,0 +1,421 @@ +"""domain层小模块批量单测. + +覆盖:recipe / tag / editing_mode / voice_library / title_library / template / edit_template +共 7 个模块,纯逻辑 0 外部依赖。 +""" + +from __future__ import annotations + +from packages.domain.editing_mode import EditingMode +from packages.domain.edit_template import EditTemplate, EditTemplateStatus +from packages.domain.recipe import Recipe, RecipeItem +from packages.domain.tag import Tag +from packages.domain.template import Template, TemplateCategory, TemplateSegment +from packages.domain.title_library import TitleLibraryItem +from packages.domain.voice_library import VoiceLibraryItem + + +class TestEditingMode: + """EditingMode 枚举测试.""" + + def test_four_modes(self): + """四种剪辑模式.""" + assert len(EditingMode) == 4 + + def test_one_take(self): + assert EditingMode.ONE_TAKE == "one_take" + + def test_pip(self): + assert EditingMode.PIP == "pip" + + def test_voice_over(self): + assert EditingMode.VOICE_OVER == "voice_over" + + def test_voice_pip(self): + assert EditingMode.VOICE_PIP == "voice_pip" + + def test_all_values_unique(self): + values = [m.value for m in EditingMode] + assert len(values) == len(set(values)) + + +class TestTag: + """Tag 测试.""" + + def test_create_valid(self): + """正常创建.""" + tag = Tag.create(user_id="u1", name=" 搞笑 ") + assert tag.user_id == "u1" + assert tag.name == "搞笑" + assert isinstance(tag.id, str) + assert len(tag.id) > 0 + assert tag.created_at is not None + + def test_create_empty_name(self): + """空名称无效.""" + try: + Tag.create("u1", "") + assert False + except ValueError as e: + assert "不能为空" in str(e) + + def test_create_whitespace_name(self): + """纯空白名称无效.""" + try: + Tag.create("u1", " ") + assert False + except ValueError as e: + assert "不能为空" in str(e) + + def test_create_unique_id(self): + t1 = Tag.create("u", "t1") + t2 = Tag.create("u", "t2") + assert t1.id != t2.id + + +class TestRecipeItem: + """RecipeItem 测试.""" + + def test_create_asset_item(self): + """素材配方项.""" + item = RecipeItem( + id="item1", + recipe_id="r1", + item_type="asset", + item_id="a1", + position=0, + ) + assert item.item_type == "asset" + assert item.item_id == "a1" + assert item.position == 0 + + def test_create_title_item(self): + """标题配方项.""" + item = RecipeItem(id="i2", recipe_id="r1", item_type="title", item_id="t1", position=1) + assert item.item_type == "title" + assert item.position == 1 + + def test_default_metadata(self): + """默认 metadata 为空 dict.""" + item = RecipeItem(id="i1", recipe_id="r1", item_type="voice", item_id="v1") + assert item.metadata_ == {} + + +class TestRecipe: + """Recipe 测试.""" + + def test_create_minimal(self): + """最简配方.""" + recipe = Recipe(id="r1", user_id="u1", name="我的配方") + assert recipe.name == "我的配方" + assert recipe.description == "" + assert recipe.template_id == "" + assert recipe.generation_params == {} + assert recipe.items == [] + assert recipe.is_active is True + assert recipe.created_at is not None + assert recipe.updated_at is not None + + def test_create_with_items(self): + """带配方项.""" + items = [ + RecipeItem(id="i1", recipe_id="r1", item_type="asset", item_id="a1", position=0), + RecipeItem(id="i2", recipe_id="r1", item_type="title", item_id="t1", position=1), + ] + recipe = Recipe(id="r1", user_id="u1", name="配方", items=items) + assert len(recipe.items) == 2 + assert recipe.items[0].item_type == "asset" + assert recipe.items[1].position == 1 + + def test_default_items_empty_list(self): + """默认 items 为空列表.""" + r1 = Recipe(id="r1", user_id="u1", name="r1") + r2 = Recipe(id="r2", user_id="u2", name="r2") + r1.items.append("fake") + assert r2.items == [] + + +class TestVoiceLibraryItem: + """VoiceLibraryItem 测试.""" + + def test_create_minimal(self): + """最简创建.""" + item = VoiceLibraryItem(id="v1", user_id="u1", name="我的配音") + assert item.name == "我的配音" + assert item.text == "" + assert item.voice_provider == "" + assert item.voice_id == "" + assert item.voice_name == "" + assert item.audio_url == "" + assert item.duration == 0 + assert item.file_size == 0 + assert item.status == "completed" + assert item.project_id is None + assert item.tags == [] + assert item.metadata_ == {} + + def test_create_full(self): + """带全部字段.""" + item = VoiceLibraryItem( + id="v1", + user_id="u1", + name="旁白", + text="大家好", + voice_provider="xf", + voice_id="v1", + voice_name="小云", + audio_url="http://x/a.mp3", + duration=10.5, + file_size=102400, + status="processing", + project_id="p1", + tags=["旁白", "正式"], + ) + assert item.text == "大家好" + assert item.duration == 10.5 + assert item.file_size == 102400 + assert item.project_id == "p1" + assert item.tags == ["旁白", "正式"] + + def test_tags_independent(self): + """不同实例的 tags 独立.""" + i1 = VoiceLibraryItem(id="v1", user_id="u", name="n1") + i2 = VoiceLibraryItem(id="v2", user_id="u", name="n2") + i1.tags.append("x") + assert i2.tags == [] + + +class TestTitleLibraryItem: + """TitleLibraryItem 测试.""" + + def test_create_required(self): + """必填字段.""" + item = TitleLibraryItem(id="t1", user_id="u1", name="爆款标题", text="这也太牛了") + assert item.name == "爆款标题" + assert item.text == "这也太牛了" + assert item.category == "default" + assert item.description == "" + assert item.tags == [] + assert item.usage_count == 0 + assert item.is_active is True + assert item.metadata_ == {} + + def test_create_full(self): + """带全部字段.""" + item = TitleLibraryItem( + id="t1", + user_id="u1", + name="科技标题", + text="震惊!", + category="tech", + description="科技类标题", + tags=["科技", "爆款"], + usage_count=100, + is_active=False, + ) + assert item.category == "tech" + assert item.usage_count == 100 + assert item.is_active is False + assert item.tags == ["科技", "爆款"] + + def test_tags_independent(self): + i1 = TitleLibraryItem(id="t1", user_id="u", name="n", text="t") + i2 = TitleLibraryItem(id="t2", user_id="u", name="n", text="t") + i1.tags.append("x") + assert i2.tags == [] + + +class TestTemplateSegment: + """TemplateSegment 测试.""" + + def test_create(self): + """正常创建.""" + seg = TemplateSegment( + id="s1", + template_id="t1", + segment_order=0, + duration_min=2.0, + duration_max=5.0, + ) + assert seg.segment_order == 0 + assert seg.duration_min == 2.0 + assert seg.duration_max == 5.0 + assert seg.material_type is None + + def test_with_material_type(self): + """带素材类型(voice_over模式).""" + seg = TemplateSegment( + id="s1", + template_id="t1", + segment_order=0, + duration_min=3.0, + duration_max=8.0, + material_type="person", + ) + assert seg.material_type == "person" + + def test_has_timestamps(self): + seg = TemplateSegment(id="s1", template_id="t1", segment_order=0, duration_min=1, duration_max=2) + assert seg.created_at is not None + assert seg.updated_at is not None + + +class TestTemplate: + """Template 测试.""" + + def test_create_minimal(self): + """最简模板.""" + tpl = Template(id="t1", user_id="u1", name="通用模板", mode="one_take") + assert tpl.name == "通用模板" + assert tpl.mode == "one_take" + assert tpl.category == "" + assert tpl.tags == [] + assert tpl.title_config == {} + assert tpl.subtitle_config == {} + assert tpl.bgm_config == {} + assert tpl.estimated_duration == 0.0 + assert tpl.segments == [] + assert tpl.is_active is True + + def test_create_with_segments(self): + """带片段.""" + segs = [ + TemplateSegment(id="s1", template_id="t1", segment_order=0, duration_min=2, duration_max=5), + TemplateSegment(id="s2", template_id="t1", segment_order=1, duration_min=3, duration_max=7), + ] + tpl = Template(id="t1", user_id="u1", name="模板", mode="pip", segments=segs) + assert len(tpl.segments) == 2 + assert tpl.segments[0].segment_order == 0 + + def test_segments_independent(self): + t1 = Template(id="t1", user_id="u", name="n1", mode="one_take") + t2 = Template(id="t2", user_id="u", name="n2", mode="pip") + t1.segments.append("fake") + assert t2.segments == [] + + +class TestTemplateCategory: + """TemplateCategory 测试.""" + + def test_create(self): + cat = TemplateCategory(id="c1", user_id="u1", name="科技") + assert cat.name == "科技" + assert cat.created_at is not None + + +class TestEditTemplateStatus: + """EditTemplateStatus 枚举测试.""" + + def test_two_statuses(self): + assert len(EditTemplateStatus) == 2 + + def test_active(self): + assert EditTemplateStatus.ACTIVE == "active" + + def test_inactive(self): + assert EditTemplateStatus.INACTIVE == "inactive" + + +class TestEditTemplate: + """EditTemplate 测试.""" + + def test_create_minimal(self): + """最简创建.""" + tpl = EditTemplate.create(name=" 通用模板 ") + assert tpl.name == "通用模板" + assert tpl.description == "" + assert tpl.template_type == "default" + assert tpl.config == {} + assert tpl.preview_url == "" + assert tpl.sort_weight == 0 + assert tpl.status == EditTemplateStatus.ACTIVE + assert isinstance(tpl.id, str) + assert len(tpl.id) > 0 + + def test_create_full(self): + """带全部字段.""" + tpl = EditTemplate.create( + name="口播模板", + description="口播类模板", + template_type="voice_over", + config={"style": "formal"}, + preview_url="https://x/preview.mp4", + sort_weight=100, + status=EditTemplateStatus.INACTIVE, + ) + assert tpl.name == "口播模板" + assert tpl.description == "口播类模板" + assert tpl.template_type == "voice_over" + assert tpl.config == {"style": "formal"} + assert tpl.preview_url == "https://x/preview.mp4" + assert tpl.sort_weight == 100 + assert tpl.status == EditTemplateStatus.INACTIVE + + def test_create_empty_name(self): + """空名称无效.""" + try: + EditTemplate.create(name="") + assert False + except ValueError as e: + assert "不能为空" in str(e) + + def test_create_whitespace_name(self): + """空白名称无效.""" + try: + EditTemplate.create(name=" ") + assert False + except ValueError as e: + assert "不能为空" in str(e) + + def test_empty_template_type_defaults(self): + """空 template_type 默认 default.""" + tpl = EditTemplate.create(name="t", template_type="") + assert tpl.template_type == "default" + + def test_whitespace_template_type_defaults(self): + """空白 template_type 默认 default.""" + tpl = EditTemplate.create(name="t", template_type=" ") + assert tpl.template_type == "default" + + def test_config_none_defaults_empty(self): + """config=None 默认为空 dict.""" + tpl = EditTemplate.create(name="t", config=None) + assert tpl.config == {} + + def test_activate(self): + """激活.""" + tpl = EditTemplate.create(name="t", status=EditTemplateStatus.INACTIVE) + old = tpl.updated_at + tpl.activate() + assert tpl.is_active is True + assert tpl.status == EditTemplateStatus.ACTIVE + assert tpl.updated_at >= old + + def test_deactivate(self): + """停用.""" + tpl = EditTemplate.create(name="t") + old = tpl.updated_at + tpl.deactivate() + assert tpl.is_active is False + assert tpl.status == EditTemplateStatus.INACTIVE + assert tpl.updated_at >= old + + def test_is_active_property(self): + """is_active 属性.""" + tpl = EditTemplate.create(name="t") + assert tpl.is_active is True + tpl.deactivate() + assert tpl.is_active is False + tpl.activate() + assert tpl.is_active is True + + def test_unique_id(self): + t1 = EditTemplate.create(name="t1") + t2 = EditTemplate.create(name="t2") + assert t1.id != t2.id + + def test_config_independent(self): + t1 = EditTemplate.create(name="t1") + t2 = EditTemplate.create(name="t2") + t1.config["k"] = "v" + assert "k" not in t2.config