ForcePilot/backend/test/unit/scheduler/adapters/persistence/test_repos.py
Kris 4c2c6b956d test: 批量修复并新增单元/集成测试用例
1.  格式化代码与参数换行,提升可读性
2.  修正测试断言与测试逻辑,覆盖更多边界场景
3.  新增外部系统告警仓库、工具服务、系统服务等测试用例
4.  完善插件注册表的重启状态检测逻辑测试
5.  更新测试断言计数与预期结果,匹配代码变更
6.  修复Instagram适配器的签名校验测试逻辑
7.  优化会话合并、上下文测试的类型检查与断言
2026-07-11 15:51:28 +08:00

642 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""SqlAlchemy 仓储适配器单元测试。
覆盖 ``yuxi.scheduler.adapters.persistence`` 下的 4 个仓储适配器:
- ``SqlAlchemyTaskRepository``:委托 ScheduledTaskRepository
- ``SqlAlchemyRunLogRepository``:委托 ScheduledTaskRunLogRepository
- ``SqlAlchemyRunLogDailyRepository``:委托 ScheduledTaskRunLogDailyRepository
- ``SqlAlchemyIdempotencyRepository``:委托 ScheduledTaskIdempotencyRepository
验证薄委托行为:调用实际仓储方法后通过 mappers 转换为 core dataclass
``commit`` 等控制参数透传,不包含业务逻辑。
"""
from __future__ import annotations
from datetime import date, datetime
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from yuxi.scheduler.adapters.persistence.sqlalchemy_idempotency_repo import (
SqlAlchemyIdempotencyRepository,
)
from yuxi.scheduler.adapters.persistence.sqlalchemy_run_log_daily_repo import (
SqlAlchemyRunLogDailyRepository,
)
from yuxi.scheduler.adapters.persistence.sqlalchemy_run_log_repo import (
SqlAlchemyRunLogRepository,
)
from yuxi.scheduler.adapters.persistence.sqlalchemy_task_repo import (
SqlAlchemyTaskRepository,
)
from yuxi.scheduler.core.models import (
DailyStat,
HandlerSummary,
ScheduledTask,
ScheduledTaskRunLog,
)
pytestmark = pytest.mark.unit
# ─── 测试辅助 ─────────────────────────────────────────────────────────────
def _make_task_orm(**overrides) -> SimpleNamespace:
"""构造类似 ORM ScheduledTask 的 SimpleNamespace。"""
defaults = {
"id": 1,
"task_id": "task-001",
"handler_name": "my_handler",
"owner_scope": "system",
"owner_id": "test:1",
"schedule_kind": "cron",
"cron_expression": "*/15 * * * *",
"run_at": None,
"tz": "Asia/Shanghai",
"payload": {"k": "v"},
"enabled": True,
"delete_after_run": False,
"block_strategy": "discard_later",
"stagger_seconds": 30,
"consecutive_errors": 0,
"status": "active",
"last_run_at": None,
"next_run_at": datetime(2024, 1, 1, 0, 15, 0),
"last_error": None,
"created_by": "system",
"updated_by": None,
"created_at": datetime(2024, 1, 1, 0, 0, 0),
"updated_at": None,
"is_deleted": 0,
"deleted_at": None,
}
defaults.update(overrides)
return SimpleNamespace(**defaults)
def _make_run_log_orm(**overrides) -> SimpleNamespace:
defaults = {
"id": 1,
"task_id": "task-001",
"run_id": "run-001",
"triggered_by": "auto",
"status": "success",
"error_message": None,
"output": {"rows": 10},
"started_at": datetime(2024, 1, 1, 0, 15, 0),
"finished_at": datetime(2024, 1, 1, 0, 15, 5),
"created_by": "system",
"updated_by": None,
"created_at": datetime(2024, 1, 1, 0, 0, 0),
"updated_at": None,
"is_deleted": 0,
"deleted_at": None,
}
defaults.update(overrides)
return SimpleNamespace(**defaults)
# ─── SqlAlchemyTaskRepository ─────────────────────────────────────────────
@pytest.mark.unit
class TestSqlAlchemyTaskRepository:
@pytest.fixture
def repo_with_mock(self):
"""构造 SqlAlchemyTaskRepository内部 ORM 仓储为 AsyncMock。"""
db = MagicMock()
with patch(
"yuxi.scheduler.adapters.persistence.sqlalchemy_task_repo.ORMScheduledTaskRepository"
) as mock_cls:
mock_orm_repo = AsyncMock()
mock_cls.return_value = mock_orm_repo
repo = SqlAlchemyTaskRepository(db)
return repo, mock_orm_repo
@pytest.mark.asyncio
async def test_create_delegates_and_maps(self, repo_with_mock):
# Arrange
repo, mock_orm = repo_with_mock
mock_orm.create = AsyncMock(return_value=_make_task_orm())
data = {"task_id": "task-001"}
# Act
result = await repo.create(data, commit=False)
# Assert
mock_orm.create.assert_awaited_once_with(data, commit=False)
assert isinstance(result, ScheduledTask)
assert result.task_id == "task-001"
@pytest.mark.asyncio
async def test_get_by_task_id_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.get_by_task_id = AsyncMock(return_value=_make_task_orm())
# Act
result = await repo.get_by_task_id("task-001", for_update=True)
# Assert
mock_orm.get_by_task_id.assert_awaited_once_with("task-001", for_update=True)
assert isinstance(result, ScheduledTask)
@pytest.mark.asyncio
async def test_get_by_task_id_returns_none_when_orm_returns_none(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.get_by_task_id = AsyncMock(return_value=None)
# Act
result = await repo.get_by_task_id("nonexistent")
# Assert
assert result is None
@pytest.mark.asyncio
async def test_get_by_id_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.get_by_id = AsyncMock(return_value=_make_task_orm())
# Act
result = await repo.get_by_id(1, for_update=False)
# Assert
mock_orm.get_by_id.assert_awaited_once_with(1, for_update=False)
assert isinstance(result, ScheduledTask)
@pytest.mark.asyncio
async def test_list_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.list = AsyncMock(return_value=([_make_task_orm(), _make_task_orm()], 2))
# Act
tasks, total = await repo.list(page=1, page_size=20)
# Assert
assert len(tasks) == 2
assert total == 2
assert all(isinstance(t, ScheduledTask) for t in tasks)
@pytest.mark.asyncio
async def test_update_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.update = AsyncMock(return_value=_make_task_orm(status="paused"))
# Act
result = await repo.update("task-001", {"status": "paused"}, commit=False)
# Assert
mock_orm.update.assert_awaited_once_with("task-001", {"status": "paused"}, commit=False)
assert isinstance(result, ScheduledTask)
assert result.status == "paused"
@pytest.mark.asyncio
async def test_delete_delegates(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.delete = AsyncMock(return_value=True)
# Act
result = await repo.delete("task-001", commit=False)
# Assert
mock_orm.delete.assert_awaited_once_with("task-001", commit=False)
assert result is True
@pytest.mark.asyncio
async def test_list_due_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.list_due = AsyncMock(return_value=[_make_task_orm()])
now = datetime(2024, 1, 1, 0, 0, 0)
# Act
result = await repo.list_due(now, limit=500)
# Assert
mock_orm.list_due.assert_awaited_once_with(now, limit=500)
assert len(result) == 1
assert isinstance(result[0], ScheduledTask)
@pytest.mark.asyncio
async def test_list_upcoming_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.list_upcoming = AsyncMock(return_value=[_make_task_orm()])
after = datetime(2024, 1, 1, 0, 0, 0)
before = datetime(2024, 1, 2, 0, 0, 0)
# Act
result = await repo.list_upcoming(after=after, before=before, limit=50)
# Assert
mock_orm.list_upcoming.assert_awaited_once_with(
after=after, before=before, limit=50, handler_name=None
)
assert len(result) == 1
@pytest.mark.asyncio
async def test_list_handler_summary_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
row = SimpleNamespace(name="h", task_count=5, last_active_at=None)
mock_orm.list_handler_summary = AsyncMock(return_value=[row])
# Act
result = await repo.list_handler_summary()
# Assert
assert len(result) == 1
assert isinstance(result[0], HandlerSummary)
assert result[0].name == "h"
@pytest.mark.asyncio
async def test_count_by_status_delegates(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.count_by_status = AsyncMock(return_value={"active": 10})
# Act
result = await repo.count_by_status(owner_scope="system")
# Assert
mock_orm.count_by_status.assert_awaited_once_with(
owner_scope="system", owner_id=None, handler_name=None
)
assert result == {"active": 10}
@pytest.mark.asyncio
async def test_acquire_for_run_delegates(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.acquire_for_run = AsyncMock(return_value=True)
now = datetime(2024, 1, 1, 0, 0, 0)
next_run = datetime(2024, 1, 1, 0, 15, 0)
# Act
result = await repo.acquire_for_run(
"task-001", run_id="run-001", now=now, next_run_at=next_run
)
# Assert
mock_orm.acquire_for_run.assert_awaited_once_with(
"task-001", run_id="run-001", now=now, next_run_at=next_run
)
assert result is True
@pytest.mark.asyncio
async def test_pause_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.pause = AsyncMock(return_value=_make_task_orm(status="paused"))
# Act
result = await repo.pause("task-001", updated_by="admin", commit=False)
# Assert
mock_orm.pause.assert_awaited_once_with("task-001", updated_by="admin", commit=False)
assert isinstance(result, ScheduledTask)
assert result.status == "paused"
@pytest.mark.asyncio
async def test_resume_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.resume = AsyncMock(return_value=_make_task_orm(status="active"))
next_run = datetime(2024, 1, 1, 0, 15, 0)
# Act
result = await repo.resume("task-001", next_run_at=next_run, updated_by="admin", commit=False)
# Assert
mock_orm.resume.assert_awaited_once_with(
"task-001", next_run_at=next_run, updated_by="admin", commit=False
)
assert isinstance(result, ScheduledTask)
@pytest.mark.asyncio
async def test_mark_dead_letter_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.mark_dead_letter = AsyncMock(return_value=_make_task_orm(status="dead_letter"))
# Act
result = await repo.mark_dead_letter("task-001", updated_by="admin", commit=False)
# Assert
mock_orm.mark_dead_letter.assert_awaited_once_with("task-001", updated_by="admin", commit=False)
assert result.status == "dead_letter"
@pytest.mark.asyncio
async def test_reset_from_dead_letter_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.reset_from_dead_letter = AsyncMock(return_value=_make_task_orm(status="active"))
next_run = datetime(2024, 1, 1, 0, 15, 0)
# Act
await repo.reset_from_dead_letter("task-001", next_run_at=next_run, updated_by="admin")
# Assert
mock_orm.reset_from_dead_letter.assert_awaited_once_with(
"task-001", next_run_at=next_run, updated_by="admin", commit=True
)
@pytest.mark.asyncio
async def test_record_run_result_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.record_run_result = AsyncMock(
return_value=_make_task_orm(consecutive_errors=1)
)
started_at = datetime(2024, 1, 1, 0, 15, 0)
# Act
result = await repo.record_run_result(
"task-001", success=False, error="boom",
started_at=started_at, updated_by="admin", commit=False
)
# Assert
mock_orm.record_run_result.assert_awaited_once_with(
"task-001", success=False, error="boom",
backoff_until=None, started_at=started_at, updated_by="admin", commit=False
)
assert result.consecutive_errors == 1
@pytest.mark.asyncio
async def test_finalize_one_shot_task_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.finalize_one_shot_task = AsyncMock(
return_value=_make_task_orm(status="paused", next_run_at=None)
)
# Act
result = await repo.finalize_one_shot_task("task-001", updated_by="admin", commit=False)
# Assert
mock_orm.finalize_one_shot_task.assert_awaited_once_with(
"task-001", updated_by="admin", commit=False
)
assert result.status == "paused"
assert result.next_run_at is None
@pytest.mark.asyncio
async def test_get_deleted_by_task_id_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.get_deleted_by_task_id = AsyncMock(return_value=_make_task_orm(is_deleted=1))
# Act
result = await repo.get_deleted_by_task_id("task-001")
# Assert
assert isinstance(result, ScheduledTask)
assert result.is_deleted == 1
@pytest.mark.asyncio
async def test_list_deleted_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.list_deleted = AsyncMock(return_value=[_make_task_orm(is_deleted=1)])
# Act
result = await repo.list_deleted(limit=10, offset=0)
# Assert
assert len(result) == 1
assert isinstance(result[0], ScheduledTask)
@pytest.mark.asyncio
async def test_hard_delete_by_id_delegates(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.hard_delete_by_id = AsyncMock(return_value=1)
# Act
result = await repo.hard_delete_by_id(1)
# Assert
mock_orm.hard_delete_by_id.assert_awaited_once_with(1, commit=True)
assert result == 1
# ─── SqlAlchemyRunLogRepository ───────────────────────────────────────────
@pytest.mark.unit
class TestSqlAlchemyRunLogRepository:
@pytest.fixture
def repo_with_mock(self):
db = MagicMock()
with patch(
"yuxi.scheduler.adapters.persistence.sqlalchemy_run_log_repo.ORMScheduledTaskRunLogRepository"
) as mock_cls:
mock_orm_repo = AsyncMock()
mock_cls.return_value = mock_orm_repo
repo = SqlAlchemyRunLogRepository(db)
return repo, mock_orm_repo
@pytest.mark.asyncio
async def test_create_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.create = AsyncMock(return_value=_make_run_log_orm())
# Act
result = await repo.create({"task_id": "t1"}, commit=False)
# Assert
mock_orm.create.assert_awaited_once_with({"task_id": "t1"}, commit=False)
assert isinstance(result, ScheduledTaskRunLog)
@pytest.mark.asyncio
async def test_get_by_run_id_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.get_by_run_id = AsyncMock(return_value=_make_run_log_orm())
# Act
result = await repo.get_by_run_id("run-001")
# Assert
mock_orm.get_by_run_id.assert_awaited_once_with("run-001")
assert isinstance(result, ScheduledTaskRunLog)
@pytest.mark.asyncio
async def test_get_by_run_id_returns_none_when_not_found(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.get_by_run_id = AsyncMock(return_value=None)
# Act
result = await repo.get_by_run_id("nonexistent")
# Assert
assert result is None
@pytest.mark.asyncio
async def test_update_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.update = AsyncMock(return_value=_make_run_log_orm(status="success"))
# Act
result = await repo.update("run-001", {"status": "success"}, commit=False)
# Assert
mock_orm.update.assert_awaited_once_with("run-001", {"status": "success"}, commit=False)
assert result.status == "success"
@pytest.mark.asyncio
async def test_list_by_task_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.list_by_task = AsyncMock(return_value=([_make_run_log_orm()], 1))
# Act
logs, total = await repo.list_by_task("task-001", page=1, page_size=20)
# Assert
assert len(logs) == 1
assert total == 1
assert isinstance(logs[0], ScheduledTaskRunLog)
@pytest.mark.asyncio
async def test_list_by_status_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.list_by_status = AsyncMock(return_value=([_make_run_log_orm()], 1))
# Act
logs, total = await repo.list_by_status(status="running", page=1, page_size=20)
# Assert
assert len(logs) == 1
assert total == 1
@pytest.mark.asyncio
async def test_get_running_by_task_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.get_running_by_task = AsyncMock(return_value=_make_run_log_orm(status="running"))
# Act
result = await repo.get_running_by_task("task-001")
# Assert
assert isinstance(result, ScheduledTaskRunLog)
assert result.status == "running"
@pytest.mark.asyncio
async def test_count_running_delegates(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.count_running = AsyncMock(return_value=3)
# Act
result = await repo.count_running()
# Assert
assert result == 3
@pytest.mark.asyncio
async def test_reclaim_stale_runs_delegates(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.reclaim_stale_runs = AsyncMock(return_value=["handler_a", "handler_b"])
stale_before = datetime(2024, 1, 1, 0, 0, 0)
# Act
result = await repo.reclaim_stale_runs(stale_before=stale_before)
# Assert
mock_orm.reclaim_stale_runs.assert_awaited_once_with(
stale_before=stale_before,
error_message="reclaimed by scheduler restart",
commit=True,
)
assert result == ["handler_a", "handler_b"]
@pytest.mark.asyncio
async def test_cleanup_old_logs_delegates(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.cleanup_old_logs = AsyncMock(return_value=10)
before = datetime(2024, 1, 1, 0, 0, 0)
# Act
result = await repo.cleanup_old_logs(before, commit=False)
# Assert
mock_orm.cleanup_old_logs.assert_awaited_once_with(before, commit=False)
assert result == 10
# ─── SqlAlchemyRunLogDailyRepository ──────────────────────────────────────
@pytest.mark.unit
class TestSqlAlchemyRunLogDailyRepository:
@pytest.fixture
def repo_with_mock(self):
db = MagicMock()
with patch(
"yuxi.scheduler.adapters.persistence.sqlalchemy_run_log_daily_repo.ORMScheduledTaskRunLogDailyRepository"
) as mock_cls:
mock_orm_repo = AsyncMock()
mock_cls.return_value = mock_orm_repo
repo = SqlAlchemyRunLogDailyRepository(db)
return repo, mock_orm_repo
@pytest.mark.asyncio
async def test_upsert_daily_stat_delegates(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.upsert_daily_stat = AsyncMock(return_value=None)
stat_date = date(2024, 1, 1)
# Act
await repo.upsert_daily_stat(stat_date, "h", "success", commit=False)
# Assert
mock_orm.upsert_daily_stat.assert_awaited_once_with(
stat_date, "h", "success", commit=False
)
@pytest.mark.asyncio
async def test_list_daily_stat_delegates_and_maps(self, repo_with_mock):
repo, mock_orm = repo_with_mock
row = SimpleNamespace(
stat_date=date(2024, 1, 1),
handler_name="h",
success_count=10,
failure_count=2,
timeout_count=1,
dead_letter_count=0,
)
mock_orm.list_daily_stat = AsyncMock(return_value=[row])
# Act
result = await repo.list_daily_stat(
start_date=date(2024, 1, 1), end_date=date(2024, 1, 31)
)
# Assert
assert len(result) == 1
assert isinstance(result[0], DailyStat)
assert result[0].success_count == 10
@pytest.mark.asyncio
async def test_cleanup_old_daily_stats_delegates(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.cleanup_old_daily_stats = AsyncMock(return_value=5)
before = date(2023, 1, 1)
# Act
result = await repo.cleanup_old_daily_stats(before, commit=False)
# Assert
mock_orm.cleanup_old_daily_stats.assert_awaited_once_with(before, commit=False)
assert result == 5
@pytest.mark.asyncio
async def test_get_deleted_by_id_passes_through_any(self, repo_with_mock):
# Arrange - core 层无对应 dataclass按端口契约以 Any 透传 ORM
repo, mock_orm = repo_with_mock
orm_obj = SimpleNamespace(id=1)
mock_orm.get_deleted_by_id = AsyncMock(return_value=orm_obj)
# Act
result = await repo.get_deleted_by_id(1)
# Assert
assert result is orm_obj
@pytest.mark.asyncio
async def test_list_deleted_passes_through_any(self, repo_with_mock):
repo, mock_orm = repo_with_mock
orm_list = [SimpleNamespace(id=1), SimpleNamespace(id=2)]
mock_orm.list_deleted = AsyncMock(return_value=orm_list)
# Act
result = await repo.list_deleted(limit=10)
# Assert
assert result == orm_list
# ─── SqlAlchemyIdempotencyRepository ──────────────────────────────────────
@pytest.mark.unit
class TestSqlAlchemyIdempotencyRepository:
@pytest.fixture
def repo_with_mock(self):
db = MagicMock()
with patch(
"yuxi.scheduler.adapters.persistence.sqlalchemy_idempotency_repo.ORMScheduledTaskIdempotencyRepository"
) as mock_cls:
mock_orm_repo = AsyncMock()
mock_cls.return_value = mock_orm_repo
repo = SqlAlchemyIdempotencyRepository(db)
return repo, mock_orm_repo
@pytest.mark.asyncio
async def test_acquire_delegates(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.acquire = AsyncMock(return_value=True)
# Act
result = await repo.acquire("key-001", task_id="t1", operation="run")
# Assert
mock_orm.acquire.assert_awaited_once_with(
"key-001", task_id="t1", operation="run", created_by=None
)
assert result is True
@pytest.mark.asyncio
async def test_update_response_delegates(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.update_response = AsyncMock(return_value=None)
# Act
await repo.update_response("key-001", {"result": "ok"})
# Assert
mock_orm.update_response.assert_awaited_once_with(
"key-001", {"result": "ok"}, updated_by=None
)
@pytest.mark.asyncio
async def test_get_delegates(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.get = AsyncMock(return_value={"result": "ok"})
# Act
result = await repo.get("key-001", operation="trigger")
# Assert
mock_orm.get.assert_awaited_once_with("key-001", operation="trigger")
assert result == {"result": "ok"}
@pytest.mark.asyncio
async def test_get_returns_none_when_not_found(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.get = AsyncMock(return_value=None)
# Act
result = await repo.get("nonexistent")
# Assert
assert result is None
@pytest.mark.asyncio
async def test_cleanup_old_delegates(self, repo_with_mock):
repo, mock_orm = repo_with_mock
mock_orm.cleanup_old = AsyncMock(return_value=20)
before = datetime(2024, 1, 1, 0, 0, 0)
# Act
result = await repo.cleanup_old(before, commit=False)
# Assert
mock_orm.cleanup_old.assert_awaited_once_with(before, commit=False)
assert result == 20