This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
from collections.abc import Iterator
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from dependencies import get_session
|
||||
from main import app
|
||||
from tests.fakes import FakeSession
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_session() -> FakeSession:
|
||||
return FakeSession()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(fake_session: FakeSession) -> Iterator[TestClient]:
|
||||
def override_get_session() -> Iterator[FakeSession]:
|
||||
yield fake_session
|
||||
|
||||
app.dependency_overrides[get_session] = override_get_session
|
||||
|
||||
with TestClient(app) as test_client:
|
||||
yield test_client
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
@@ -0,0 +1,48 @@
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from models import Todo
|
||||
|
||||
|
||||
class FakeScalarResult:
|
||||
def __init__(self, todos: list[Todo]) -> None:
|
||||
self.todos = todos
|
||||
|
||||
def all(self) -> list[Todo]:
|
||||
return self.todos
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self) -> None:
|
||||
self.todo_to_return: Todo | None = None
|
||||
self.added_todo: Todo | None = None
|
||||
self.committed = False
|
||||
self.deleted_todo: Todo | None = None
|
||||
self.should_fail_commit = False
|
||||
self.rolled_back = False
|
||||
self.todos_to_return: list[Todo] = []
|
||||
|
||||
def add(self, todo: Todo) -> None:
|
||||
self.added_todo = todo
|
||||
todo.id = 1
|
||||
todo.completion = False
|
||||
|
||||
def commit(self) -> None:
|
||||
if self.should_fail_commit:
|
||||
raise SQLAlchemyError("simulated commit failure")
|
||||
|
||||
self.committed = True
|
||||
|
||||
def get(
|
||||
self,
|
||||
_model: type[Todo],
|
||||
_todo_id: int,
|
||||
) -> Todo | None:
|
||||
return self.todo_to_return
|
||||
|
||||
def delete(self, todo: Todo) -> None:
|
||||
self.deleted_todo = todo
|
||||
|
||||
def rollback(self) -> None:
|
||||
self.rolled_back = True
|
||||
|
||||
def scalars(self, _statement: object) -> FakeScalarResult:
|
||||
return FakeScalarResult(self.todos_to_return)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
def test_health_returns_success(client: TestClient) -> None:
|
||||
response = client.get("/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
||||
|
||||
|
||||
def test_unknown_route_returns_404(client: TestClient) -> None:
|
||||
response = client.get("/not-exist")
|
||||
|
||||
assert response.status_code == 404
|
||||
@@ -0,0 +1,28 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from main import todocreate
|
||||
|
||||
|
||||
def test_todocreate_requires_task() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
todocreate()
|
||||
|
||||
def test_todocreate_accepts_valid_task() -> None:
|
||||
todo_data = todocreate(task = "学习测试单元")
|
||||
|
||||
assert todo_data.task == "学习测试单元"
|
||||
|
||||
def test_todocreate_rejects_empty_task() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
todocreate(task="")
|
||||
|
||||
def test_todocreate_rejects_blank_task() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
todocreate(task=" ")
|
||||
|
||||
def test_todocreate_rejects_task_longer_than_200_characters() -> None:
|
||||
long_task = "a" * 201
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
todocreate(task=long_task)
|
||||
@@ -0,0 +1,353 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from models import Todo
|
||||
from tests.fakes import FakeSession
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_task",
|
||||
[
|
||||
"",
|
||||
" ",
|
||||
"a" * 201,
|
||||
],
|
||||
)
|
||||
def test_create_todo_rejects_invalid_task(
|
||||
client: TestClient,
|
||||
invalid_task:str,
|
||||
) -> None:
|
||||
response = client.post(
|
||||
"/todos",
|
||||
json={"task": invalid_task},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
def test_create_todo_rejects_missing_task(
|
||||
client: TestClient,
|
||||
) -> None:
|
||||
response = client.post(
|
||||
"/todos",
|
||||
json={},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
error = response.json()["detail"][0]
|
||||
|
||||
assert error["loc"] == ["body", "task"]
|
||||
assert error["type"] == "missing"
|
||||
|
||||
def test_create_todo_success(
|
||||
client: TestClient,
|
||||
fake_session: FakeSession,
|
||||
) -> None:
|
||||
response = client.post(
|
||||
"/todos",
|
||||
json={"task": "学习单元测试"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"id": 1,
|
||||
"task": "学习单元测试",
|
||||
"completion": False,
|
||||
}
|
||||
assert fake_session.committed is True
|
||||
assert fake_session.added_todo.task == "学习单元测试"
|
||||
|
||||
def test_patch_missing_todo_returns_404(
|
||||
client: TestClient,
|
||||
fake_session: FakeSession,
|
||||
) -> None:
|
||||
fake_session.todo_to_return = None
|
||||
|
||||
response = client.patch(
|
||||
"/todos/999",
|
||||
json={"task": "修改后的任务"},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {
|
||||
"detail": "Todo not found",
|
||||
}
|
||||
assert fake_session.committed is False
|
||||
assert fake_session.rolled_back is False
|
||||
|
||||
def test_patch_existing_todo_updates_task(
|
||||
client: TestClient,
|
||||
fake_session: FakeSession,
|
||||
) -> None:
|
||||
fake_session.todo_to_return = Todo(
|
||||
id=1,
|
||||
task="旧任务",
|
||||
completion=False,
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
"/todos/1",
|
||||
json={"task": "新任务"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"id": 1,
|
||||
"task": "新任务",
|
||||
"completion": False,
|
||||
}
|
||||
assert fake_session.committed is True
|
||||
assert fake_session.todo_to_return.task == "新任务"
|
||||
|
||||
def test_patch_existing_todo_updates_completion_only(
|
||||
client: TestClient,
|
||||
fake_session: FakeSession,
|
||||
) -> None:
|
||||
fake_session.todo_to_return = Todo(
|
||||
id=1,
|
||||
task="保留原任务",
|
||||
completion=False,
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
"/todos/1",
|
||||
json={"completion": True},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"id": 1,
|
||||
"task": "保留原任务",
|
||||
"completion": True,
|
||||
}
|
||||
assert fake_session.committed is True
|
||||
assert fake_session.todo_to_return.task == "保留原任务"
|
||||
assert fake_session.todo_to_return.completion is True
|
||||
|
||||
def test_delete_existing_todo_success(
|
||||
client: TestClient,
|
||||
fake_session: FakeSession,
|
||||
) -> None:
|
||||
existing_todo = Todo(
|
||||
id=1,
|
||||
task="准备删除的任务",
|
||||
completion=False,
|
||||
)
|
||||
fake_session.todo_to_return = existing_todo
|
||||
|
||||
response = client.delete("/todos/1")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"id": 1,
|
||||
"task": "准备删除的任务",
|
||||
"completion": False,
|
||||
}
|
||||
assert fake_session.committed is True
|
||||
assert fake_session.deleted_todo is existing_todo
|
||||
|
||||
def test_delete_missing_todo_returns_404(
|
||||
client: TestClient,
|
||||
fake_session: FakeSession,
|
||||
) -> None:
|
||||
response = client.delete("/todos/999")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {
|
||||
"detail": "Todo not found",
|
||||
}
|
||||
|
||||
assert fake_session.deleted_todo is None
|
||||
assert fake_session.committed is False
|
||||
|
||||
def test_create_todo_rolls_back_when_commit_fails(
|
||||
client: TestClient,
|
||||
fake_session: FakeSession,
|
||||
) -> None:
|
||||
fake_session.should_fail_commit = True
|
||||
|
||||
with pytest.raises(SQLAlchemyError):
|
||||
client.post(
|
||||
"/todos",
|
||||
json={"task": "会提交失败的任务"},
|
||||
)
|
||||
|
||||
assert fake_session.rolled_back is True
|
||||
assert fake_session.committed is False
|
||||
|
||||
def test_patch_todo_rolls_back_when_commit_fails(
|
||||
client: TestClient,
|
||||
fake_session: FakeSession,
|
||||
) -> None:
|
||||
fake_session.todo_to_return = Todo(
|
||||
id=1,
|
||||
task="旧任务",
|
||||
completion=False,
|
||||
)
|
||||
fake_session.should_fail_commit = True
|
||||
|
||||
with pytest.raises(SQLAlchemyError):
|
||||
client.patch(
|
||||
"/todos/1",
|
||||
json={"task": "新任务"},
|
||||
)
|
||||
|
||||
assert fake_session.rolled_back is True
|
||||
assert fake_session.committed is False
|
||||
|
||||
def test_delete_todo_rolls_back_when_commit_fails(
|
||||
client: TestClient,
|
||||
fake_session: FakeSession,
|
||||
) -> None:
|
||||
existing_todo = Todo(
|
||||
id=1,
|
||||
task="准备删除的任务",
|
||||
completion=False,
|
||||
)
|
||||
fake_session.todo_to_return = existing_todo
|
||||
fake_session.should_fail_commit = True
|
||||
|
||||
with pytest.raises(SQLAlchemyError):
|
||||
client.delete("/todos/1")
|
||||
|
||||
assert fake_session.deleted_todo is existing_todo
|
||||
assert fake_session.rolled_back is True
|
||||
assert fake_session.committed is False
|
||||
|
||||
def test_get_todos_returns_todo_list(
|
||||
client: TestClient,
|
||||
fake_session: FakeSession,
|
||||
) -> None:
|
||||
fake_session.todos_to_return = [
|
||||
Todo(
|
||||
id=1,
|
||||
task="学习单元测试",
|
||||
completion=False,
|
||||
),
|
||||
Todo(
|
||||
id=2,
|
||||
task="完成接口测试",
|
||||
completion=True,
|
||||
),
|
||||
]
|
||||
|
||||
response = client.get("/todos")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [
|
||||
{
|
||||
"id": 1,
|
||||
"task": "学习单元测试",
|
||||
"completion": False,
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"task": "完成接口测试",
|
||||
"completion": True,
|
||||
},
|
||||
]
|
||||
|
||||
def test_get_todos_returns_empty_list(
|
||||
client: TestClient,
|
||||
fake_session: FakeSession,
|
||||
) -> None:
|
||||
fake_session.todos_to_return = []
|
||||
|
||||
response = client.get("/todos")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_task",
|
||||
[
|
||||
"",
|
||||
" ",
|
||||
"a" * 201,
|
||||
],
|
||||
)
|
||||
def test_patch_todo_rejects_invalid_task(
|
||||
client: TestClient,
|
||||
fake_session: FakeSession,
|
||||
invalid_task: str,
|
||||
) -> None:
|
||||
fake_session.todo_to_return = Todo(
|
||||
id=1,
|
||||
task="原任务",
|
||||
completion=False,
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
"/todos/1",
|
||||
json={"task": invalid_task},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
def test_patch_todo_rejects_empty_body(
|
||||
client: TestClient,
|
||||
fake_session: FakeSession,
|
||||
) -> None:
|
||||
fake_session.todo_to_return = Todo(
|
||||
id=1,
|
||||
task="原任务",
|
||||
completion=False,
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
"/todos/1",
|
||||
json={},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert fake_session.committed is False
|
||||
|
||||
def test_patch_todo_accepts_completion_false(
|
||||
client: TestClient,
|
||||
fake_session: FakeSession,
|
||||
) -> None:
|
||||
fake_session.todo_to_return = Todo(
|
||||
id=1,
|
||||
task="已有任务",
|
||||
completion=True,
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
"/todos/1",
|
||||
json={"completion": False},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert fake_session.todo_to_return.completion is False
|
||||
assert response.json()["completion"] is False
|
||||
assert fake_session.committed is True
|
||||
|
||||
def test_patch_todo_updates_task_and_completion(
|
||||
client: TestClient,
|
||||
fake_session: FakeSession,
|
||||
) -> None:
|
||||
existing_todo = Todo(
|
||||
id=1,
|
||||
task="原任务",
|
||||
completion=False,
|
||||
)
|
||||
fake_session.todo_to_return = existing_todo
|
||||
|
||||
response = client.patch(
|
||||
"/todos/1",
|
||||
json={
|
||||
"task": "修改后的任务",
|
||||
"completion": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"id": 1,
|
||||
"task": "修改后的任务",
|
||||
"completion": True,
|
||||
}
|
||||
assert existing_todo.task == "修改后的任务"
|
||||
assert existing_todo.completion is True
|
||||
assert fake_session.committed is True
|
||||
@@ -0,0 +1,23 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from main import todoupdate
|
||||
|
||||
|
||||
def test_todoupdate_rejects_empty_update() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
todoupdate()
|
||||
|
||||
def test_todoupdate_accepts_provided_value() -> None:
|
||||
update_data = todoupdate(
|
||||
task = "学习测试单元",
|
||||
completion = True,
|
||||
)
|
||||
|
||||
assert update_data.task == "学习测试单元"
|
||||
|
||||
def test_todoupdate_accepts_task_only() -> None:
|
||||
update_data = todoupdate(task="修改后的任务")
|
||||
|
||||
assert update_data.task == "修改后的任务"
|
||||
assert update_data.completion is None
|
||||
Reference in New Issue
Block a user