Skip to content

Commit ae26c92

Browse files
cguarinosuclaude
andcommitted
test: Add comprehensive unit tests with mocking
Implement complete test coverage for all API endpoints using pytest with proper mocking and dependency injection override patterns. Test structure: - tests/test_todo_lists.py: 11 test cases organized by endpoint - Uses FastAPI TestClient for HTTP testing - Mock TodoListService to isolate controller logic - Dependency override pattern for clean testing Test coverage: ✅ GET /api/todolists (Index) - Returns all todo lists successfully - Returns empty list when no todos exist ✅ GET /api/todolists/{id} (Show) - Returns specific todo list by ID - Returns 404 when todo not found ✅ POST /api/todolists (Create) - Creates new todo list successfully - Validates required fields (422 error) - Validates name not empty (422 error) ✅ PUT /api/todolists/{id} (Update) - Updates existing todo list successfully - Returns 404 when todo not found - Validates required fields (422 error) ✅ DELETE /api/todolists/{id} (Delete) - Deletes existing todo list (204 response) - Returns 404 when todo not found Testing patterns: - Fixture-based test client and mock service - AAA pattern (Arrange, Act, Assert) - Proper assertion of status codes and response data - Mock verification with assert_called_once() - Class-based test organization (TestIndex, TestShow, etc.) Configuration: - pytest.ini_options in pyproject.toml - asyncio_mode = "auto" for async test support - Strict markers and config enforcement 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8cd2884 commit ae26c92

File tree

2 files changed

+244
-0
lines changed

2 files changed

+244
-0
lines changed

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Test suite."""

tests/test_todo_lists.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
"""Unit tests for TodoList API endpoints."""
2+
3+
from typing import Generator
4+
from unittest.mock import MagicMock
5+
6+
import pytest
7+
from fastapi.testclient import TestClient
8+
9+
from app.main import app
10+
from app.models import TodoList
11+
from app.services.todo_lists import get_todo_list_service
12+
13+
14+
@pytest.fixture
15+
def mock_service() -> Generator[MagicMock, None, None]:
16+
"""
17+
Create a mock TodoListService for testing.
18+
19+
Yields:
20+
Mock service instance
21+
"""
22+
mock = MagicMock()
23+
24+
# Override the dependency
25+
def override_get_service() -> MagicMock:
26+
return mock
27+
28+
app.dependency_overrides[get_todo_list_service] = override_get_service
29+
yield mock
30+
app.dependency_overrides.clear()
31+
32+
33+
@pytest.fixture
34+
def client() -> TestClient:
35+
"""
36+
Create a test client for the FastAPI app.
37+
38+
Returns:
39+
TestClient instance
40+
"""
41+
return TestClient(app)
42+
43+
44+
class TestIndex:
45+
"""Tests for GET /api/todolists endpoint."""
46+
47+
def test_returns_all_todo_lists(
48+
self, client: TestClient, mock_service: MagicMock
49+
) -> None:
50+
"""Test that index returns all todo lists."""
51+
# Arrange
52+
expected_todos = [
53+
TodoList(id=1, name="First list"),
54+
TodoList(id=2, name="Second list"),
55+
]
56+
mock_service.all.return_value = expected_todos
57+
58+
# Act
59+
response = client.get("/api/todolists")
60+
61+
# Assert
62+
assert response.status_code == 200
63+
assert len(response.json()) == 2
64+
assert response.json()[0]["id"] == 1
65+
assert response.json()[0]["name"] == "First list"
66+
assert response.json()[1]["id"] == 2
67+
assert response.json()[1]["name"] == "Second list"
68+
mock_service.all.assert_called_once()
69+
70+
def test_returns_empty_list_when_no_todos(
71+
self, client: TestClient, mock_service: MagicMock
72+
) -> None:
73+
"""Test that index returns empty list when no todos exist."""
74+
# Arrange
75+
mock_service.all.return_value = []
76+
77+
# Act
78+
response = client.get("/api/todolists")
79+
80+
# Assert
81+
assert response.status_code == 200
82+
assert response.json() == []
83+
mock_service.all.assert_called_once()
84+
85+
86+
class TestShow:
87+
"""Tests for GET /api/todolists/{id} endpoint."""
88+
89+
def test_returns_todo_list_by_id(
90+
self, client: TestClient, mock_service: MagicMock
91+
) -> None:
92+
"""Test that show returns a specific todo list."""
93+
# Arrange
94+
expected_todo = TodoList(id=1, name="Test list")
95+
mock_service.get.return_value = expected_todo
96+
97+
# Act
98+
response = client.get("/api/todolists/1")
99+
100+
# Assert
101+
assert response.status_code == 200
102+
assert response.json()["id"] == 1
103+
assert response.json()["name"] == "Test list"
104+
mock_service.get.assert_called_once_with(1)
105+
106+
def test_returns_404_when_not_found(
107+
self, client: TestClient, mock_service: MagicMock
108+
) -> None:
109+
"""Test that show returns 404 when todo list doesn't exist."""
110+
# Arrange
111+
mock_service.get.return_value = None
112+
113+
# Act
114+
response = client.get("/api/todolists/999")
115+
116+
# Assert
117+
assert response.status_code == 404
118+
assert "not found" in response.json()["detail"].lower()
119+
mock_service.get.assert_called_once_with(999)
120+
121+
122+
class TestCreate:
123+
"""Tests for POST /api/todolists endpoint."""
124+
125+
def test_creates_new_todo_list(
126+
self, client: TestClient, mock_service: MagicMock
127+
) -> None:
128+
"""Test that create successfully creates a new todo list."""
129+
# Arrange
130+
created_todo = TodoList(id=1, name="New list")
131+
mock_service.create.return_value = created_todo
132+
133+
# Act
134+
response = client.post("/api/todolists", json={"name": "New list"})
135+
136+
# Assert
137+
assert response.status_code == 201
138+
assert response.json()["id"] == 1
139+
assert response.json()["name"] == "New list"
140+
mock_service.create.assert_called_once()
141+
142+
def test_validates_required_fields(
143+
self, client: TestClient, mock_service: MagicMock
144+
) -> None:
145+
"""Test that create validates required fields."""
146+
# Act
147+
response = client.post("/api/todolists", json={})
148+
149+
# Assert
150+
assert response.status_code == 422
151+
mock_service.create.assert_not_called()
152+
153+
def test_validates_name_not_empty(
154+
self, client: TestClient, mock_service: MagicMock
155+
) -> None:
156+
"""Test that create validates name is not empty."""
157+
# Act
158+
response = client.post("/api/todolists", json={"name": ""})
159+
160+
# Assert
161+
assert response.status_code == 422
162+
mock_service.create.assert_not_called()
163+
164+
165+
class TestUpdate:
166+
"""Tests for PUT /api/todolists/{id} endpoint."""
167+
168+
def test_updates_existing_todo_list(
169+
self, client: TestClient, mock_service: MagicMock
170+
) -> None:
171+
"""Test that update successfully updates an existing todo list."""
172+
# Arrange
173+
updated_todo = TodoList(id=1, name="Updated list")
174+
mock_service.update.return_value = updated_todo
175+
176+
# Act
177+
response = client.put("/api/todolists/1", json={"name": "Updated list"})
178+
179+
# Assert
180+
assert response.status_code == 200
181+
assert response.json()["id"] == 1
182+
assert response.json()["name"] == "Updated list"
183+
mock_service.update.assert_called_once()
184+
185+
def test_returns_404_when_not_found(
186+
self, client: TestClient, mock_service: MagicMock
187+
) -> None:
188+
"""Test that update returns 404 when todo list doesn't exist."""
189+
# Arrange
190+
mock_service.update.return_value = None
191+
192+
# Act
193+
response = client.put("/api/todolists/999", json={"name": "Updated"})
194+
195+
# Assert
196+
assert response.status_code == 404
197+
assert "not found" in response.json()["detail"].lower()
198+
mock_service.update.assert_called_once()
199+
200+
def test_validates_required_fields(
201+
self, client: TestClient, mock_service: MagicMock
202+
) -> None:
203+
"""Test that update validates required fields."""
204+
# Act
205+
response = client.put("/api/todolists/1", json={})
206+
207+
# Assert
208+
assert response.status_code == 422
209+
mock_service.update.assert_not_called()
210+
211+
212+
class TestDelete:
213+
"""Tests for DELETE /api/todolists/{id} endpoint."""
214+
215+
def test_deletes_existing_todo_list(
216+
self, client: TestClient, mock_service: MagicMock
217+
) -> None:
218+
"""Test that delete successfully deletes an existing todo list."""
219+
# Arrange
220+
mock_service.delete.return_value = True
221+
222+
# Act
223+
response = client.delete("/api/todolists/1")
224+
225+
# Assert
226+
assert response.status_code == 204
227+
assert response.content == b""
228+
mock_service.delete.assert_called_once_with(1)
229+
230+
def test_returns_404_when_not_found(
231+
self, client: TestClient, mock_service: MagicMock
232+
) -> None:
233+
"""Test that delete returns 404 when todo list doesn't exist."""
234+
# Arrange
235+
mock_service.delete.return_value = False
236+
237+
# Act
238+
response = client.delete("/api/todolists/999")
239+
240+
# Assert
241+
assert response.status_code == 404
242+
assert "not found" in response.json()["detail"].lower()
243+
mock_service.delete.assert_called_once_with(999)

0 commit comments

Comments
 (0)