~75 min
Write unit and end-to-end tests, diagnose bugs from failing test output, and use an AI agent to generate additional test cases.
The back-end contains intentional bugs that you will discover and fix by writing tests, then use an AI agent to generate additional coverage.
sequenceDiagram
actor Developer
participant Local as "Local Machine<br/>(pytest)"
participant GitHub as "GitHub<br/>(origin)"
participant VM as "VM<br/>(Docker / FastAPI)"
participant AI as "AI Agent"
Note over Developer,VM: Part A: Unit Tests (Local)
Developer->>Local: uv run poe test-unit
Local-->>Developer: 3 passed
Developer->>Local: Add boundary test
Developer->>Local: uv run poe test-unit
Local-->>Developer: FAILED: assert 0 == 1
Developer->>Local: Fix bug (< to <=)
Developer->>Local: uv run poe test-unit
Local-->>Developer: 4 passed
Developer->>GitHub: git push
Developer->>VM: git pull, docker compose up app --build -d
Note over Developer,VM: Part B: End-to-End Tests (Local to VM)
Developer->>Local: uv run poe test-e2e
Local->>VM: GET /interactions/
VM-->>Local: 500 Internal Server Error
Local-->>Developer: FAILED: assert 500 == 200
Developer->>VM: docker compose logs app
VM-->>Developer: ResponseValidationError:<br/>timestamp field missing
Developer->>Local: Fix bug (timestamp to created_at)
Developer->>GitHub: git push
Developer->>VM: git pull, docker compose up app --build -d
Developer->>Local: uv run poe test-e2e
Local->>VM: GET /interactions/
VM-->>Local: 200 OK
Local-->>Developer: 5 passed
Note over Developer,AI: Part C: AI-Generated Tests
Developer->>AI: Generate 10 unit tests
AI-->>Developer: test_interactions_ai.py
Developer->>Developer: Review and curate
Developer->>Local: uv run poe test
Local-->>Developer: All tests passed
- 1. Steps
- 2. Acceptance criteria
Follow the Git workflow to complete this task.
Important
You work on the <task-branch>.
Title: [Task] Back-end Testing
- 1.3.1. Create the environment file for unit tests
- 1.3.2. Run all unit tests
- 1.3.3. Add a new unit test
- 1.3.4. Fix the bug
- 1.3.5. Rerun unit tests
- 1.3.6. Commit changes
- 1.3.7. Transfer the changes to your VM
- 1.3.8. Deploy the fixed
appservice on your VM
Note
Unit tests do not require a running server. They test individual functions in isolation.
-
To copy the content of
.env.tests.unit.exampleto.env.tests.unit.secret,cp .env.tests.unit.example .env.tests.unit.secret
-
To run all unit tests,
uv run poe test-unitSee
poe test-unit. -
All tests should pass.
The output should be similar to this:
===================== 3 passed in X.XXs =====================
Tip
Feel free to use AI to generate the tests. Make sure to provide them with necessary context.
-
Add a new unit test that targets the following boundary-value case:
An interaction whose
item_idis exactly equal tomax_item_id— for example,item_id=2andmax_item_id=2. This interaction should appear in the results because the filter condition is "less than or equal to."Name the test
test_filter_includes_interaction_at_boundary. -
Click to open a hint
The code for the new case should be almost the same as for the existing tests.
-
Click to open the solution
def test_filter_includes_interaction_at_boundary() -> None: interactions = [_make_log(1, 1, 2)] result = filter_by_max_item_id(interactions=interactions, max_item_id=2) assert len(result) == 1 assert result[0].id == 1
-
To run the tests,
uv run poe test-unit -
Observe that the new test fails.
The output should be similar to this:
FAILED backend/tests/unit/test_interactions.py::test_filter_includes_interaction_at_boundary - assert 0 == 1This line means the following:
- The test failed (
FAILED). - The test is in the file
backend/tests/unit/test_interactions.py. - The name of the failing test is
test_filter_includes_interaction_at_boundary. - The failed assertion is
assert 0 == 1— the filter returned 0 interactions, but 1 was expected.
- The test failed (
-
Fix the bug in the
filter_by_max_item_idfunction. -
Click to open a hint
The filter is applied in-memory after all interactions are fetched from the database. Look at the comparison operator in the condition that decides which interactions to include — it excludes the boundary value.
-
Click to open the solution
Find this line in
filter_by_max_item_id:return [i for i in interactions if i.item_id < max_item_id]
Change it to:
return [i for i in interactions if i.item_id <= max_item_id]
-
To rerun the unit tests,
uv run poe test-unit -
All tests should pass.
The output should be similar to this:
===================== 4 passed in X.XXs =====================
-
Use this commit message:
fix: use <= instead of < in max_item_id filter
Note
See origin.
-
Push commits to
<task-branch>onorigin.Replace
<task-branch>. -
To navigate to the repository directory on the VM,
cd se-toolkit-lab-4 -
Pull the latest changes from
<task-branch>onorigin.Replace
<task-branch>. -
Replace
<task-branch>.
-
To rebuild and start the
appservice in background,docker compose --env-file .env.docker.secret up app --build --force-recreate -d -
To verify the service is running,
docker compose --env-file .env.docker.secret ps app --format "table {{.Service}}\t{{.Status}}"The output should be similar to this:
SERVICE STATUS app Up 1 minute
- 1.4.1. Create the environment file for end-to-end tests
- 1.4.2. Run existing end-to-end tests
- 1.4.3. Add three end-to-end tests
- 1.4.4. Diagnose the bug
- 1.4.5. Fix the bug
- 1.4.6. Redeploy and rerun
- 1.4.7. Commit the fix
Note
End-to-end (E2E) tests run on your local machine and send real HTTP requests to the deployed version on the VM.
In Part A, a unit test caught a logic error inside a single function. Some bugs only appear when all components interact during a real request — for example, a mismatch between layers of the stack. End-to-end tests catch these integration-level failures.
-
To copy the content of
.env.tests.e2e.exampleto.env.tests.e2e.secret,cp .env.tests.e2e.example .env.tests.e2e.secret -
Open
.env.tests.e2e.secret. -
Set
API_BASE_URLtohttp://<your-vm-ip-address>:<caddy-port>. Replace: -
Set
API_KEYto the same value asAPI_KEYin.env.docker.secreton the VM.
-
To run the end-to-end tests,
uv run poe test-e2eSee
poe test-e2e. -
All existing end-to-end tests should pass.
The output should be similar to this:
===================== 2 passed in X.XXs =====================
-
Add three end-to-end tests that cover the following cases:
- Test 1:
GET /interactions/returnsHTTPstatus code200. - Test 2:
GET /interactions/response items contain the expected fields (id,item_id,created_at). - Test 3:
GET /interactions/?max_item_id=1returns a non-empty list where every item hasitem_idless than or equal to1. This confirms the Part A fix at the API level.
Click to open the solution
import httpx def test_get_interactions_returns_200(client: httpx.Client) -> None: response = client.get("/interactions/") assert response.status_code == 200 def test_get_interactions_response_items_have_expected_fields(client: httpx.Client) -> None: response = client.get("/interactions/") data = response.json() assert len(data) > 0 assert "id" in data[0] assert "item_id" in data[0] assert "created_at" in data[0] def test_get_interactions_filter_includes_boundary(client: httpx.Client) -> None: response = client.get("/interactions/?max_item_id=1") assert response.status_code == 200 data = response.json() assert len(data) > 0 assert all(item["item_id"] <= 1 for item in data)
- Test 1:
-
To run the end-to-end tests,
uv run poe test-e2e -
Observe that all three new tests fail.
The output should be similar to this:
FAILED backend/tests/e2e/test_interactions.py::test_get_interactions_returns_200 - assert 500 == 200 FAILED backend/tests/e2e/test_interactions.py::test_get_interactions_response_items_have_expected_fields - json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) FAILED backend/tests/e2e/test_interactions.py::test_get_interactions_filter_includes_boundary - assert 500 == 200The
500status code means the server encountered an internal error while building the response.
-
Switch to the
VS Code Terminalconnected to your VM (the one you used in section 1.3.8). -
To read the recent
appservice logs,docker compose --env-file .env.docker.secret logs app --tail 30 -
Look for a traceback near the bottom of the output.
The output should include lines similar to this:
app-1 | fastapi.exceptions.ResponseValidationError: 12 validation errors: app-1 | {'type': 'missing', 'loc': ('response', 0, 'timestamp'), 'msg': 'Field required', 'input': InteractionLog(kind='view', id=1, learner_id=1, item_id=1, created_at=datetime.datetime(2025, 9, 2, 10, 0))} ... app-1 | {'type': 'missing', 'loc': ('response', 11, 'timestamp'), 'msg': 'Field required', 'input': InteractionLog(kind='view', id=12, learner_id=5, item_id=6, created_at=datetime.datetime(2025, 10, 16, 9, 30))} app-1 | app-1 | File "/app/backend/app/routers/interactions.py", line 26, in get_interactionsThis tells you that something bad happened in
backend/app/routers/interactions.pyat the line 26.Namely, the
timestampfield is missing butInteractionLoghas only thecreated_atfield. -
Look at the code on that line:
@router.get("/", response_model=list[InteractionModel]) -
Look at the code inside
get_interactions.First,
interactionsare read from the database.interactions = await read_interactions(session)
Then, they're filtered:
return filter_by_max_item_id(interactions, max_item_id)
The type of the returned value is
list[InteractionLog].Look at the definition of
InteractionLoginbackend/app/models/interaction.py.It has the field
created_at.Now, look at the definition of
InteractionModelbelow in the same file.It has all the same fields as
InteractionLogexcept it hastimestampinstead ofcreated_at.So,
FastAPItries but can't convertInteractionLogtoInteractionModelbecause of this field name mismatch.
-
Based on the traceback, fix the bug in
InteractionModel. -
Click to open the solution
Find this line in
InteractionModel:timestamp: datetime
Change it to:
created_at: datetime
-
To run the end-to-end tests,
uv run poe test-e2e -
All end-to-end tests should pass.
The output should be similar to this:
===================== 5 passed in X.XXs =====================
-
Use this commit message:
fix: rename timestamp to created_at in InteractionModel
Important
Each fix must be a separate commit. Do not combine the Part A and Part B fixes into one commit.
- 1.5.1. Generate tests
- 1.5.2. Review and curate the tests
- 1.5.3. Commit the curated tests
- 1.5.4. Run the full test suite
- 1.5.5. Address the testing findings
-
Open the coding agent.
-
Give it this prompt:
Read the back-end source code under
backend/and the existing unit tests inbackend/tests/unit/test_interactions.py.Generate ten new unit tests that cover edge cases and boundary values not already tested.
Write them to a new file
backend/tests/unit/test_interactions_ai.py. -
Wait for the agent to generate the tests.
-
Open the file:
backend/tests/unit/test_interactions_ai.py. -
Review each generated test against the following criteria:
- Keep — the test is correct, targets a real case, and adds coverage not already present.
- Fix — the test has a minor error (wrong assertion, wrong expected value) but the idea is sound — correct it.
- Discard — the test duplicates an existing test, is logically wrong, or tests behaviour outside the scope of the module.
-
Keep at least two tests and discard at least one.
-
For each generated test, add a one-line comment that documents your decision and reason:
- For a kept or fixed test:
# KEPT: <reason>or# FIXED: <reason> - For a discarded test: paste it commented out with
# DISCARDED: <reason>above it
Example:
# KEPT: covers the empty-list edge case, not tested elsewhere def test_filter_returns_empty_list_when_no_interactions() -> None: ... # DISCARDED: duplicates test_filter_includes_interaction_at_boundary # def test_filter_boundary_duplicate() -> None: # ...
- For a kept or fixed test:
Note
Commit new tests to be able to improve them later without losing the history of changes.
-
Use the following commit message:
test: add curated AI-generated unit tests
-
To run the full test suite,
uv run poe testSee
poe test. -
All tests (including the curated AI-generated ones) should pass.
-
If a test fails, decide whether the test or the implementation is wrong.
-
If the test is flawed, go back to 1.5.2 and fix or discard it.
-
If the test reveals a real bug:
- Fix the implementation.
- Commit changes.
- Create a PR with your changes.
- Get a PR review and complete the subsequent steps in the
Git workflow.
Check the task using the autochecker Telegram bot.
- Issue has the correct title.
- All unit tests pass.
- All end-to-end tests pass.
- AI-generated tests include at least two kept tests and at least one discarded test.
- Each kept, fixed, or discarded test has a one-line comment explaining the decision.
- The Part A fix and Part B fix are separate commits.
- PR is approved.
- PR is merged.