Skip to content

Commit cbd7fed

Browse files
committed
Refactor test setup and enhance documentation for Python testing strategies
1 parent 16e6d4e commit cbd7fed

File tree

3 files changed

+289
-23
lines changed

3 files changed

+289
-23
lines changed

document/06-testing.en.md

Lines changed: 134 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,97 @@ func TestSayHelloWithTime(t *testing.T) {
233233

234234
This demonstrates writing code with testing in mind.
235235

236+
### Python
237+
238+
# Python Testing Strategies
239+
240+
**:book: Reference**
241+
242+
(EN)[pytest: helps you write better programs — pytest documentation](https://docs.pytest.org/en/stable/)
243+
(EN)pytest fixtures: explicit, modular, scalable — pytest documentation
244+
(EN)Parametrizing fixtures and test functions — pytest documentation
245+
246+
While Python has the standard unittest library built-in for testing, the **pytest** library is widely used to write more flexible and readable tests. pytest comes with a simple API and powerful features, and can be easily installed with `pip install pytest`. You can run tests with the `$ pytest` command.
247+
248+
In Python, you can use the `pytest.mark.parametrize` decorator to describe multiple test cases together. Let's write a test for the say_hello function:
249+
250+
```python
251+
# hello.py
252+
def say_hello(name=""):
253+
if name:
254+
return f"Hello, {name}!"
255+
return "Hello!"
256+
257+
# test_hello.py
258+
import pytest
259+
from hello import say_hello
260+
261+
@pytest.mark.parametrize("name, expected",[
262+
("Alice", "Hello, Alice!"),
263+
("", "Hello!"),
264+
]
265+
)
266+
def test_say_hello(name, expected):
267+
got = say_hello(name)
268+
269+
# Check if the expected return value and the actual value are the same, and display an error if they differ
270+
assert got == expected, f"unexpected result of say_hello: want={expected}, got={got}"
271+
```
272+
273+
The need to consider argument design with testing in mind is common to both Python and Go. Let's consider modifying the `say_hello` implementation to change the greeting based on the time of day:
274+
275+
```python
276+
from datetime import datetime
277+
278+
def say_hello(name):
279+
now = datetime.now() # Directly depends on the current time, making it difficult to test
280+
current_hour = now.hour
281+
282+
if 6 <= current_hour < 10:
283+
return f"Good morning, {name}!"
284+
if 10 <= current_hour < 18:
285+
return f"Hello, {name}!"
286+
return f"Good evening, {name}!"
287+
```
288+
289+
This function is difficult to test because it directly depends on the current time. To test each time period, you would need to run the test at that specific time.
290+
291+
To make it more testable, we can rewrite the function as follows:
292+
293+
```python
294+
# Improved code (more testable design)
295+
from datetime import datetime
296+
297+
def say_hello(name, now=None):
298+
if now is None:
299+
now = datetime.now()
300+
301+
current_hour = now.hour
302+
303+
if 6 <= current_hour < 10:
304+
return f"Good morning, {name}!"
305+
if 10 <= current_hour < 18:
306+
return f"Hello, {name}!"
307+
return f"Good evening, {name}!"
308+
```
309+
310+
Now we can specify the current time as an argument. By setting None as the default value, we can still omit the now parameter in normal usage.
311+
312+
```python
313+
import pytest
314+
from datetime import datetime
315+
from greetings import say_hello
316+
317+
@pytest.mark.parametrize("name, now, expected", [
318+
("Alice", datetime(2024, 1, 1, 9, 0, 0), "Good morning, Alice!"),
319+
("Bob", datetime(2024, 1, 1, 12, 0, 0), "Hello, Bob!"),
320+
("Charlie", datetime(2024, 1, 1, 20, 0, 0), "Good evening, Charlie!"),
321+
])
322+
def test_say_hello_simple(name, now, expected):
323+
got = say_hello(name, now)
324+
assert got == expected, f"unexpected result of say_hello: want={expected}, got={got}"
325+
```
326+
236327
## 1. Writing Tests for the Item Listing API
237328

238329
Let's write tests for basic functionality, specifically testing item registration requests.
@@ -251,8 +342,11 @@ Let's write test cases for this.
251342
- What does this test verify?
252343
- What's the difference between `t.Error()` and `t.Fatal()`?
253344

254-
### Python
255-
TBD
345+
### Python (Read Only)
346+
347+
Python testing is implemented in [`main_test.py`](https://github.com/mercari-build/mercari-build-training/blob/main/python/main_test.py).
348+
349+
Unlike the Go API implementation, in Python API implementation using the FastAPI framework, developers do not need to implement HTTP Request parsing themselves. Therefore, no additional implementation is required in this chapter, but you should review the test code to deepen your understanding.
256350

257351
## 2. Writing Tests for the Hello Handler
258352

@@ -283,7 +377,16 @@ Once you have the logic figured out, implement it.
283377

284378
### Python
285379

286-
TBD
380+
- (En)[FastAPI > Learn > Tutorial - User Guide / Testing](https://fastapi.tiangolo.com/tutorial/testing/)
381+
382+
In Python, we use FastAPI's `testclient.TestClient` to verify that the handler function `hello` works correctly. Let's edit the test function [test_hello](https://github.com/mercari-build/mercari-build-training/blob/main/python/main_test.py#L53) that's already provided and write a test.
383+
384+
As with Go, let's implement the test code with the following considerations in mind:
385+
386+
- What do you want to test with this handler?
387+
- How can you verify that it behaves correctly?
388+
389+
For implementing the test, you may refer to the [official FastAPI documentation](https://fastapi.tiangolo.com/tutorial/testing/#testclient).
287390

288391
## 3. Writing Tests Using Mocks
289392

@@ -311,9 +414,24 @@ Let's test both successful and failed persistence scenarios using mocks.
311414
- Consider the benefits of using interfaces to satisfy mocks
312415
- Think about the pros and cons of using mocks
313416

314-
### Python
417+
### Python (Read Only)
315418

316-
TBD
419+
**:book: Reference**
420+
421+
- (EN) [pytest-mock](https://github.com/pytest-dev/pytest-mock)
422+
- (EN) [unittest.mock --- introduction](https://docs.python.org/3.13/library/unittest.mock-examples.html#)
423+
424+
For Python mock libraries, there are several options including the built-in standard `unittest.mock` and pytest's `pytest-mock`. Mocks become necessary when the process being tested depends on external tools or objects, such as in the following cases:
425+
426+
- Mocking database connections to test user authentication logic without connecting to an actual database.
427+
- Mocking HTTP API clients to test weather forecast retrieval functions without actual network communication.
428+
- Mocking the file system to test logging functionality without actual file operations.
429+
430+
In our case, we could consider implementing a test like the first example mentioned: "mocking database connections." However, the Build Python API implementation is very simple, and setting up classes like ItemRepository for mock testing would unnecessarily complicate the implementation.
431+
432+
Since sufficient verification can be done with the test code implemented in the chapter "4. Writing tests using actual databases," and because it would contradict Python's language philosophy of "simplicity" and "explicitness," **we have omitted Python implementations using mocks from this teaching material**.
433+
434+
However, in actual development environments where applications become more complex, there are many cases where tests using mocks are implemented in Python as well. If you're interested, take a look at the explanation of mock testing in the Go section, or review Python test implementations using mocks that are introduced on the internet.
317435

318436
## 4. Writing Tests Using Real Databases
319437

@@ -332,7 +450,17 @@ After performing database operations, we need to verify the database state match
332450

333451
### Python
334452

335-
TBD
453+
Let's write a test in Python using a test database (sqlite3). Uncomment the two places in [main_test.py](https://github.com/mercari-build/mercari-build-training/blob/main/python/main_test.py) that say "`STEP 6-4: uncomment this test setup`". ([first location](https://github.com/mercari-build/mercari-build-training/blob/main/python/main_test.py#L9-L42)/[second location](https://github.com/mercari-build/mercari-build-training/blob/main/python/main_test.py#L60-L84))
454+
455+
The `db_connection` function creates and sets up a new test database using sqlite3 before the test, and deletes the test database after the test is completed.
456+
457+
The `test_add_item_e2e` function tests the item addition functionality by sending a POST request to the API endpoint (`/items/`). This function runs with parameterized test cases (valid and invalid data). The test verifies:
458+
459+
1. Whether the response status code matches the expected value
460+
2. For non-error cases, whether the response body contains a "message"
461+
3. Whether the data was correctly saved in the database (matching name and category)
462+
463+
What's particularly important is that it tests end-to-end using an actual database (for testing) rather than mocks, which verifies the functionality in a way that's closer to the actual environment.
336464

337465
## Next
338466

0 commit comments

Comments
 (0)