diff --git a/tests/unit/test_title_library_use_cases.py b/tests/unit/test_title_library_use_cases.py index 5843f149c..e4aa0b9db 100755 --- a/tests/unit/test_title_library_use_cases.py +++ b/tests/unit/test_title_library_use_cases.py @@ -1,4 +1,4 @@ -"""标题库 UseCase 单元测试.""" +"""标题库 Use Cases 单元测试 — wave217""" from __future__ import annotations @@ -24,384 +24,447 @@ from packages.application.title_library.use_cases import ( from packages.domain.exceptions import NotFoundError, QuotaExceededError from packages.domain.title_library import TitleLibraryItem +# ── helpers ────────────────────────────────────────────────────────────────── -def _make_item(id: str, name: str, text: str, usage_count: int = 0, category: str = "default") -> TitleLibraryItem: + +def _make_item( + item_id="t1", + user_id="u1", + name="标题A", + text="这是一个标题", + category="default", + usage_count=0, + is_active=True, + description="", + tags=None, + metadata_=None, +): return TitleLibraryItem( - id=id, - user_id="user_1", + id=item_id, + user_id=user_id, name=name, text=text, category=category, - description="", - tags=[], + description=description, + tags=tags or [], usage_count=usage_count, - is_active=True, - metadata_={}, + is_active=is_active, + metadata_=metadata_ or {}, ) -@pytest.fixture -def mock_repo(): - return MagicMock() - - -@pytest.fixture -def sample_item(): - return _make_item("title_1", "爆款标题", "这是一个爆款标题文案", usage_count=5) +# ── ListTitleLibraryUseCase ───────────────────────────────────────────────── class TestListTitleLibraryUseCase: - """ListTitleLibraryUseCase 测试""" + def test_list_default_params(self): + items = [_make_item("t1"), _make_item("t2")] + repo = MagicMock() + repo.list_by_user.return_value = items - def test_list_returns_results(self, mock_repo, sample_item): - """正常返回标题列表""" - mock_repo.list_by_user.return_value = [sample_item] - use_case = ListTitleLibraryUseCase(mock_repo) + uc = ListTitleLibraryUseCase(repo) + result = uc.execute("u1") - result = use_case.execute("user_1") + assert len(result) == 2 + repo.list_by_user.assert_called_once_with("u1", category=None, skip=0, limit=50) - assert len(result) == 1 - assert result[0].id == "title_1" - mock_repo.list_by_user.assert_called_once_with("user_1", category=None, skip=0, limit=50) + def test_list_with_category(self): + repo = MagicMock() + repo.list_by_user.return_value = [] - def test_list_with_category(self, mock_repo, sample_item): - """按分类过滤""" - mock_repo.list_by_user.return_value = [sample_item] - use_case = ListTitleLibraryUseCase(mock_repo) - - use_case.execute("user_1", category="电商") - - mock_repo.list_by_user.assert_called_once_with("user_1", category="电商", skip=0, limit=50) - - def test_list_with_pagination(self, mock_repo, sample_item): - """带分页参数""" - mock_repo.list_by_user.return_value = [sample_item] - use_case = ListTitleLibraryUseCase(mock_repo) - - use_case.execute("user_1", skip=10, limit=20) - - mock_repo.list_by_user.assert_called_once_with("user_1", category=None, skip=10, limit=20) - - def test_empty_list(self, mock_repo): - """空列表""" - mock_repo.list_by_user.return_value = [] - use_case = ListTitleLibraryUseCase(mock_repo) - - result = use_case.execute("user_1") + uc = ListTitleLibraryUseCase(repo) + result = uc.execute("u1", category="marketing") + repo.list_by_user.assert_called_once_with("u1", category="marketing", skip=0, limit=50) assert result == [] + def test_list_pagination(self): + repo = MagicMock() + repo.list_by_user.return_value = [] + + uc = ListTitleLibraryUseCase(repo) + uc.execute("u1", skip=10, limit=20) + repo.list_by_user.assert_called_once_with("u1", category=None, skip=10, limit=20) + + +# ── GetTitleLibraryUseCase ────────────────────────────────────────────────── + class TestGetTitleLibraryUseCase: - """GetTitleLibraryUseCase 测试""" + def test_get_found(self): + item = _make_item() + repo = MagicMock() + repo.get.return_value = item - def test_get_existing(self, mock_repo, sample_item): - """获取存在的标题""" - mock_repo.get.return_value = sample_item - use_case = GetTitleLibraryUseCase(mock_repo) + uc = GetTitleLibraryUseCase(repo) + result = uc.execute("t1", "u1") + assert result.id == "t1" + repo.get.assert_called_once_with("t1", "u1") - result = use_case.execute("title_1", "user_1") - - assert result is not None - assert result.id == "title_1" - mock_repo.get.assert_called_once_with("title_1", "user_1") - - def test_get_nonexistent_returns_none(self, mock_repo): - """获取不存在的标题返回 None""" - mock_repo.get.return_value = None - use_case = GetTitleLibraryUseCase(mock_repo) - - result = use_case.execute("nonexistent", "user_1") + def test_get_not_found(self): + repo = MagicMock() + repo.get.return_value = None + uc = GetTitleLibraryUseCase(repo) + result = uc.execute("t999", "u1") assert result is None +# ── CreateTitleLibraryUseCase ─────────────────────────────────────────────── + + class TestCreateTitleLibraryUseCase: - """CreateTitleLibraryUseCase 测试""" + def test_create_success_free_plan_within_quota(self): + repo = MagicMock() + repo.count_by_user.return_value = 0 # 已用数量 + repo.create.side_effect = lambda x: x - def test_create_success(self, mock_repo, sample_item): - """创建成功""" - mock_repo.count_by_user.return_value = 0 - mock_repo.create.return_value = sample_item - use_case = CreateTitleLibraryUseCase(mock_repo) + uc = CreateTitleLibraryUseCase(repo) + cmd = CreateTitleLibraryCommand(user_id="u1", name="好标题", text="这是一个好标题的内容") - command = CreateTitleLibraryCommand( - user_id="user_1", - name="新标题", - text="新标题文案", - category="default", - description="", - tags=[], - metadata_={}, + with patch("packages.application.title_library.use_cases.quota_checker") as mock_qc: + mock_result = MagicMock() + mock_result.allowed = True + mock_result.limit = 10 + mock_result.used = 0 + mock_qc.check.return_value = mock_result + + result = uc.execute(cmd, plan_name="free") + + assert result.name == "好标题" + assert result.user_id == "u1" + repo.create.assert_called_once() + + def test_create_quota_exceeded_raises(self): + repo = MagicMock() + repo.count_by_user.return_value = 100 + + uc = CreateTitleLibraryUseCase(repo) + cmd = CreateTitleLibraryCommand(user_id="u1", name="超了", text="配额超限了") + + with patch("packages.application.title_library.use_cases.quota_checker") as mock_qc: + mock_result = MagicMock() + mock_result.allowed = False + mock_result.limit = 5 + mock_result.used = 5 + mock_qc.check.return_value = mock_result + + with pytest.raises(QuotaExceededError): + uc.execute(cmd, plan_name="free") + + def test_create_with_tags_and_metadata(self): + repo = MagicMock() + repo.count_by_user.return_value = 0 + repo.create.side_effect = lambda x: x + + uc = CreateTitleLibraryUseCase(repo) + cmd = CreateTitleLibraryCommand( + user_id="u1", + name="标题", + text="内容", + category="marketing", + description="描述", + tags=["tag1", "tag2"], + metadata_={"key": "value"}, ) - result = use_case.execute(command, plan_name="free") - assert result.id == "title_1" - mock_repo.count_by_user.assert_called_once_with("user_1") - mock_repo.create.assert_called_once() + with patch("packages.application.title_library.use_cases.quota_checker") as mock_qc: + mock_result = MagicMock() + mock_result.allowed = True + mock_result.limit = 100 + mock_result.used = 0 + mock_qc.check.return_value = mock_result - def test_create_quota_exceeded(self, mock_repo): - """超过配额时抛出 QuotaExceededError""" - mock_repo.count_by_user.return_value = 9999 - use_case = CreateTitleLibraryUseCase(mock_repo) + result = uc.execute(cmd) - command = CreateTitleLibraryCommand( - user_id="user_1", - name="新标题", - text="文案", - category="default", - description="", - tags=[], - metadata_={}, - ) - with pytest.raises(QuotaExceededError): - use_case.execute(command, plan_name="free") + assert result.category == "marketing" + assert result.description == "描述" + assert result.tags == ["tag1", "tag2"] + assert result.metadata_ == {"key": "value"} - mock_repo.create.assert_not_called() - def test_create_with_tags_and_metadata(self, mock_repo, sample_item): - """创建时带 tags 和 metadata_""" - mock_repo.count_by_user.return_value = 0 - mock_repo.create.return_value = sample_item - use_case = CreateTitleLibraryUseCase(mock_repo) - - command = CreateTitleLibraryCommand( - user_id="user_1", - name="带标签标题", - text="文案", - category="电商", - description="测试描述", - tags=["爆款", "促销"], - metadata_={"source": "manual"}, - ) - use_case.execute(command, plan_name="premium") - - created = mock_repo.create.call_args[0][0] - assert isinstance(created, TitleLibraryItem) - assert created.name == "带标签标题" - assert created.category == "电商" - assert created.tags == ["爆款", "促销"] - assert created.metadata_ == {"source": "manual"} +# ── UpdateTitleLibraryUseCase ─────────────────────────────────────────────── class TestUpdateTitleLibraryUseCase: - """UpdateTitleLibraryUseCase 测试""" + def test_update_name(self): + existing = _make_item(name="old") + repo = MagicMock() + repo.get.return_value = existing + repo.update.return_value = existing - def test_update_name(self, mock_repo, sample_item): - """更新标题名称""" - mock_repo.get.return_value = sample_item - mock_repo.update.side_effect = lambda x: x - use_case = UpdateTitleLibraryUseCase(mock_repo) + uc = UpdateTitleLibraryUseCase(repo) + cmd = UpdateTitleLibraryCommand(title_id="t1", user_id="u1", name="new") + result = uc.execute(cmd) - command = UpdateTitleLibraryCommand(title_id="title_1", user_id="user_1", name="新名称") - result = use_case.execute(command) + assert result.name == "new" + repo.update.assert_called_once() - assert result.name == "新名称" - # 其他字段不变 - assert result.text == "这是一个爆款标题文案" - mock_repo.get.assert_called_once_with("title_1", "user_1") - mock_repo.update.assert_called_once() + def test_update_text(self): + existing = _make_item(text="old") + repo = MagicMock() + repo.get.return_value = existing + repo.update.return_value = existing - def test_update_multiple_fields(self, mock_repo, sample_item): - """同时更新多个字段""" - mock_repo.get.return_value = sample_item - mock_repo.update.side_effect = lambda x: x - use_case = UpdateTitleLibraryUseCase(mock_repo) + uc = UpdateTitleLibraryUseCase(repo) + cmd = UpdateTitleLibraryCommand(title_id="t1", user_id="u1", text="new text") + result = uc.execute(cmd) + assert result.text == "new text" - command = UpdateTitleLibraryCommand( - title_id="title_1", - user_id="user_1", - text="新文案内容", - category="美食", - is_active=False, - ) - result = use_case.execute(command) + def test_update_category(self): + existing = _make_item(category="old") + repo = MagicMock() + repo.get.return_value = existing + repo.update.return_value = existing - assert result.text == "新文案内容" - assert result.category == "美食" + uc = UpdateTitleLibraryUseCase(repo) + cmd = UpdateTitleLibraryCommand(title_id="t1", user_id="u1", category="new_cat") + result = uc.execute(cmd) + assert result.category == "new_cat" + + def test_update_tags(self): + existing = _make_item(tags=["old"]) + repo = MagicMock() + repo.get.return_value = existing + repo.update.return_value = existing + + uc = UpdateTitleLibraryUseCase(repo) + cmd = UpdateTitleLibraryCommand(title_id="t1", user_id="u1", tags=["a", "b"]) + result = uc.execute(cmd) + assert result.tags == ["a", "b"] + + def test_update_is_active(self): + existing = _make_item(is_active=True) + repo = MagicMock() + repo.get.return_value = existing + repo.update.return_value = existing + + uc = UpdateTitleLibraryUseCase(repo) + cmd = UpdateTitleLibraryCommand(title_id="t1", user_id="u1", is_active=False) + result = uc.execute(cmd) assert result.is_active is False - def test_update_nonexistent_raises(self, mock_repo): - """更新不存在的标题抛出 NotFoundError""" - mock_repo.get.return_value = None - use_case = UpdateTitleLibraryUseCase(mock_repo) + def test_update_not_found_raises(self): + repo = MagicMock() + repo.get.return_value = None - command = UpdateTitleLibraryCommand(title_id="noexist", user_id="user_1", name="新名称") - with pytest.raises(NotFoundError, match="not found"): - use_case.execute(command) + uc = UpdateTitleLibraryUseCase(repo) + cmd = UpdateTitleLibraryCommand(title_id="t999", user_id="u1", name="x") + with pytest.raises(NotFoundError): + uc.execute(cmd) - mock_repo.update.assert_not_called() + def test_update_none_fields_not_modified(self): + existing = _make_item(name="keep", category="keep_cat", description="keep_desc") + repo = MagicMock() + repo.get.return_value = existing + repo.update.return_value = existing + + uc = UpdateTitleLibraryUseCase(repo) + cmd = UpdateTitleLibraryCommand(title_id="t1", user_id="u1") # 全None + result = uc.execute(cmd) + + assert result.name == "keep" + assert result.category == "keep_cat" + assert result.description == "keep_desc" + + +# ── DeleteTitleLibraryUseCase ─────────────────────────────────────────────── class TestDeleteTitleLibraryUseCase: - """DeleteTitleLibraryUseCase 测试""" - - def test_delete_success(self, mock_repo): - """删除成功""" - mock_repo.delete.return_value = True - use_case = DeleteTitleLibraryUseCase(mock_repo) - - result = use_case.execute("title_1", "user_1") + def test_delete_success(self): + repo = MagicMock() + repo.delete.return_value = True + uc = DeleteTitleLibraryUseCase(repo) + result = uc.execute("t1", "u1") assert result is True - mock_repo.delete.assert_called_once_with("title_1", "user_1") + repo.delete.assert_called_once_with("t1", "u1") - def test_delete_nonexistent_returns_false(self, mock_repo): - """删除不存在的返回 False""" - mock_repo.delete.return_value = False - use_case = DeleteTitleLibraryUseCase(mock_repo) - - result = use_case.execute("noexist", "user_1") + def test_delete_not_found(self): + repo = MagicMock() + repo.delete.return_value = False + uc = DeleteTitleLibraryUseCase(repo) + result = uc.execute("t999", "u1") assert result is False +# ── IncrementTitleUsageUseCase ────────────────────────────────────────────── + + class TestIncrementTitleUsageUseCase: - """IncrementTitleUsageUseCase 测试""" + def test_increment_default_1(self): + repo = MagicMock() + repo.increment_usage_count.return_value = True - def test_increment_positive(self, mock_repo): - """正增量时调用 repository""" - mock_repo.increment_usage_count.return_value = True - use_case = IncrementTitleUsageUseCase(mock_repo) - - command = IncrementTitleUsageCommand(title_id="title_1", user_id="user_1", increment=1) - result = use_case.execute(command) + uc = IncrementTitleUsageUseCase(repo) + cmd = IncrementTitleUsageCommand(title_id="t1", user_id="u1") + result = uc.execute(cmd) assert result is True - mock_repo.increment_usage_count.assert_called_once_with("title_1", "user_1", increment=1) + repo.increment_usage_count.assert_called_once_with("t1", "u1", increment=1) - def test_increment_zero_returns_false(self, mock_repo): - """增量为0返回False,不调用repository""" - use_case = IncrementTitleUsageUseCase(mock_repo) + def test_increment_custom_amount(self): + repo = MagicMock() + repo.increment_usage_count.return_value = True - command = IncrementTitleUsageCommand(title_id="title_1", user_id="user_1", increment=0) - result = use_case.execute(command) + uc = IncrementTitleUsageUseCase(repo) + cmd = IncrementTitleUsageCommand(title_id="t1", user_id="u1", increment=5) + result = uc.execute(cmd) + + repo.increment_usage_count.assert_called_once_with("t1", "u1", increment=5) + + def test_increment_zero_returns_false(self): + repo = MagicMock() + + uc = IncrementTitleUsageUseCase(repo) + cmd = IncrementTitleUsageCommand(title_id="t1", user_id="u1", increment=0) + result = uc.execute(cmd) assert result is False - mock_repo.increment_usage_count.assert_not_called() + repo.increment_usage_count.assert_not_called() - def test_increment_negative_returns_false(self, mock_repo): - """负增量返回False""" - use_case = IncrementTitleUsageUseCase(mock_repo) + def test_increment_negative_returns_false(self): + repo = MagicMock() - command = IncrementTitleUsageCommand(title_id="title_1", user_id="user_1", increment=-1) - result = use_case.execute(command) + uc = IncrementTitleUsageUseCase(repo) + cmd = IncrementTitleUsageCommand(title_id="t1", user_id="u1", increment=-1) + result = uc.execute(cmd) assert result is False - mock_repo.increment_usage_count.assert_not_called() + repo.increment_usage_count.assert_not_called() - def test_increment_large_number(self, mock_repo): - """大增量值""" - mock_repo.increment_usage_count.return_value = True - use_case = IncrementTitleUsageUseCase(mock_repo) - command = IncrementTitleUsageCommand(title_id="title_1", user_id="user_1", increment=10) - use_case.execute(command) - - mock_repo.increment_usage_count.assert_called_once_with("title_1", "user_1", increment=10) +# ── PickTitleUseCase ──────────────────────────────────────────────────────── class TestPickTitleUseCase: - """PickTitleUseCase 智能选标题测试""" + def test_pick_from_multiple_returns_least_used_in_pool(self): + items = [ + _make_item("t1", usage_count=10), + _make_item("t2", usage_count=1), # 最少 + _make_item("t3", usage_count=5), + _make_item("t4", usage_count=3), + _make_item("t5", usage_count=8), + _make_item("t6", usage_count=2), + ] + repo = MagicMock() + repo.list_by_user.return_value = items - def test_pick_from_multiple(self, mock_repo): - """从多个标题中选一个(最少使用的前5个中随机)""" - items = [_make_item(f"t{i}", f"标题{i}", f"文案{i}", usage_count=i) for i in range(10)] - mock_repo.list_by_user.return_value = items - use_case = PickTitleUseCase(mock_repo) + uc = PickTitleUseCase(repo) + cmd = PickTitleCommand(user_id="u1") - command = PickTitleCommand(user_id="user_1") - result = use_case.execute(command) + # 由于有随机性,多次验证都在候选池(最少使用的5个)中 + for _ in range(10): + result = uc.execute(cmd) + assert result is not None + # 最少使用的5个是: t2(1), t6(2), t4(3), t3(5), t5(8) + assert result.id in {"t1", "t2", "t3", "t4", "t5", "t6"} + # 选中的一定是使用次数最少的5个之一 (usage_count <= 8) + assert result.usage_count <= 8 - assert result is not None - assert isinstance(result, TitleLibraryItem) - # 选出的应该是使用次数最少的前5个之一(0-4) - assert result.usage_count <= 4 - mock_repo.list_by_user.assert_called_once() + # 验证查询参数 + repo.list_by_user.assert_called() + call_args = repo.list_by_user.call_args + assert call_args[0][0] == "u1" + assert call_args[1]["is_active"] is True - def test_pick_empty_returns_none(self, mock_repo): - """空标题库返回 None""" - mock_repo.list_by_user.return_value = [] - use_case = PickTitleUseCase(mock_repo) - - command = PickTitleCommand(user_id="user_1") - result = use_case.execute(command) + def test_pick_empty_returns_none(self): + repo = MagicMock() + repo.list_by_user.return_value = [] + uc = PickTitleUseCase(repo) + cmd = PickTitleCommand(user_id="u1") + result = uc.execute(cmd) assert result is None - def test_pick_with_category(self, mock_repo): - """按分类选标题""" - items = [_make_item("t1", "标题1", "文案1", category="美食")] - mock_repo.list_by_user.return_value = items - use_case = PickTitleUseCase(mock_repo) + def test_pick_single_item(self): + item = _make_item("t1") + repo = MagicMock() + repo.list_by_user.return_value = [item] - command = PickTitleCommand(user_id="user_1", category="美食") - result = use_case.execute(command) + uc = PickTitleUseCase(repo) + cmd = PickTitleCommand(user_id="u1") + result = uc.execute(cmd) + assert result.id == "t1" + + def test_pick_with_category_filter(self): + repo = MagicMock() + repo.list_by_user.return_value = [_make_item("t1", category="marketing")] + + uc = PickTitleUseCase(repo) + cmd = PickTitleCommand(user_id="u1", category="marketing") + result = uc.execute(cmd) assert result is not None - call_kwargs = mock_repo.list_by_user.call_args[1] - assert call_kwargs["category"] == "美食" - assert call_kwargs["is_active"] is True + repo.list_by_user.assert_called_once() + assert repo.list_by_user.call_args[1]["category"] == "marketing" - def test_pick_exclude_ids(self, mock_repo): - """排除指定ID""" + def test_pick_exclude_ids(self): items = [ - _make_item("t1", "标题1", "文案1", usage_count=1), - _make_item("t2", "标题2", "文案2", usage_count=2), - _make_item("t3", "标题3", "文案3", usage_count=3), + _make_item("t1", usage_count=1), + _make_item("t2", usage_count=2), + _make_item("t3", usage_count=3), ] - mock_repo.list_by_user.return_value = items - use_case = PickTitleUseCase(mock_repo) + repo = MagicMock() + repo.list_by_user.return_value = items - command = PickTitleCommand(user_id="user_1", exclude_ids=["t1", "t2"]) - result = use_case.execute(command) + uc = PickTitleUseCase(repo) + cmd = PickTitleCommand(user_id="u1", exclude_ids=["t1", "t2"]) - # 排除两个后只剩t3 + # 排除 t1, t2 后只剩 t3 + result = uc.execute(cmd) assert result.id == "t3" - def test_pick_exclude_all_falls_back(self, mock_repo): - """排除全部时从所有标题中选""" + def test_pick_exclude_all_fallback_to_all(self): items = [ - _make_item("t1", "标题1", "文案1", usage_count=1), - _make_item("t2", "标题2", "文案2", usage_count=2), + _make_item("t1", usage_count=1), + _make_item("t2", usage_count=2), ] - mock_repo.list_by_user.return_value = items - use_case = PickTitleUseCase(mock_repo) + repo = MagicMock() + repo.list_by_user.return_value = items - command = PickTitleCommand(user_id="user_1", exclude_ids=["t1", "t2"]) - result = use_case.execute(command) + uc = PickTitleUseCase(repo) + cmd = PickTitleCommand(user_id="u1", exclude_ids=["t1", "t2"]) - # 排除全部后fallback到全部,所以还是能选出一个 + # 排除后没了,回退到从全部选 + result = uc.execute(cmd) assert result is not None - assert result.id in ("t1", "t2") + assert result.id in {"t1", "t2"} - def test_pick_single_item(self, mock_repo): - """只有一个标题时选它""" - item = _make_item("only", "唯一标题", "唯一文案", usage_count=10) - mock_repo.list_by_user.return_value = [item] - use_case = PickTitleUseCase(mock_repo) + def test_pick_pool_size_is_5(self): + # 10个标题,使用次数从 1~10 + items = [_make_item(f"t{i}", usage_count=i) for i in range(1, 11)] + repo = MagicMock() + repo.list_by_user.return_value = items - command = PickTitleCommand(user_id="user_1") - result = use_case.execute(command) + uc = PickTitleUseCase(repo) + cmd = PickTitleCommand(user_id="u1") - assert result.id == "only" - - def test_pick_prefers_less_used(self, mock_repo): - """倾向于选择使用次数少的""" - items = [ - _make_item("t_used", "常用", "常用", usage_count=100), - _make_item("t_fresh", "新的", "新的", usage_count=0), - ] - mock_repo.list_by_user.return_value = items - use_case = PickTitleUseCase(mock_repo) - - # 跑多次,验证使用少的出现在候选池里 - results = set() + # 运行多次,确保选中的都在前5个使用最少的里(t1~t5, usage 1~5) for _ in range(20): - command = PickTitleCommand(user_id="user_1") - r = use_case.execute(command) - if r: - results.add(r.id) + result = uc.execute(cmd) + assert int(result.id[1:]) <= 5 # 只从前5个里选 - # 两个都在候选池(少于5个),所以都可能被选中 - assert "t_used" in results or "t_fresh" in results + def test_pick_fewer_than_pool_size(self): + # 只有3个标题,不足5个池大小 + items = [ + _make_item("t1", usage_count=3), + _make_item("t2", usage_count=1), + _make_item("t3", usage_count=2), + ] + repo = MagicMock() + repo.list_by_user.return_value = items + + uc = PickTitleUseCase(repo) + cmd = PickTitleCommand(user_id="u1") + + results = set() + for _ in range(30): + result = uc.execute(cmd) + results.add(result.id) + + # 3个都有可能被选中(随机性+少量样本,大概率至少出现2个) + assert len(results) >= 1 + assert results.issubset({"t1", "t2", "t3"})