Testing & QA with Gemini CLI
TaskFlow already has 8 tests. But which lines of code do those 8 tests leave completely uncovered? Which error branches have never been triggered? Which auth edge cases could silently break? This module shows you how to use Gemini CLI to answer all three questions — and fill the gaps — in under an hour.
What You'll Learn
- Ask Gemini to generate targeted pytest tests from TaskFlow's actual router code
- Identify untested error paths by pointing Gemini at
routers/tasks.py - Feed failing test output back to Gemini and get a diagnosis with a fix
- Run
pytest --covand ask Gemini which uncovered lines matter most - Generate auth tests covering duplicate email, wrong password, and expired JWT
- Understand mutation testing and use Gemini to verify test strength
- Complete the lab: push TaskFlow from ~65% to 90%+ coverage
The Testing Gap
Before: Imagine a night watchman who patrols a building every night, but only ever checks the front door. He marks "all clear" in his log. Management assumes the building is secure.
Pain: One morning there's a break-in through a side window. The watchman's log was accurate — but his patrol route was incomplete. The system wasn't broken; the coverage was.
Mapping: A test suite that only covers the happy path is exactly that watchman. Your 8 existing TaskFlow tests check the main flows: create, list, get, update, delete. But list_tasks has 4 filter parameters. create_task has a tag-lookup branch. get_current_user has 3 separate ways to fail. Those are the side windows — and right now, nobody is checking them.
The typical developer-written test suite achieves 65% line coverage. That sounds reasonable until you realize the uncovered 35% is almost always the error-handling branches — exactly the code that runs when something goes wrong in production.
A 2023 study of 500 production incidents found that 72% of bugs occurred in code paths that had no test coverage. For TaskFlow specifically: the priority filter in list_tasks accepts any string — passing ?priority=urgent (not a valid value) silently returns an empty list. Is that the right behavior? Without a test, you won't know until a user files a bug report.
Before: 8 hand-written tests — click Play to see the gap close
Generate Tests From Implementation
The key insight: Gemini CLI can read your implementation file alongside your existing conftest and generate tests that match your exact fixture setup. You don't need to explain what auth_headers does — it sees it.
A fixture is a function marked with @pytest.fixture that sets up and tears down test state. In TaskFlow's conftest.py, the auth_headers fixture registers a test user, logs them in, and returns the Authorization: Bearer ... header dictionary — so every test that uses it starts already authenticated. The client fixture creates a FastAPI TestClient that routes requests to an in-memory SQLite database instead of the real one.
Prompting for list_tasks coverage
Open your terminal in sample-project/taskflow/ and run:
gemini "@routers/tasks.py @tests/conftest.py Generate pytest tests for the list_tasks endpoint covering:
1. Happy path — user gets back their own tasks
2. Filter by completed=true
3. Filter by priority=low
4. Filter by tag name
5. Unauthenticated request returns 401
6. Invalid priority value returns empty list (not an error)"
@ references. Gemini reads the implementation to know what filters exist, and reads conftest to know the fixture signatures to use.Gemini will produce something like this. Read it line by line — every test is grounded in the actual TaskFlow data model:
import pytest
# ── CHUNK 1: Setup helper ────────────────────────────────────────
# WHAT: Creates a task via the API (not direct DB insert) so the
# owner_id is always set to the authenticated test user.
# WHY: Using the API instead of inserting directly ensures we're
# testing the real code path, not bypassing it.
def create_task(client, headers, **kwargs):
payload = {"title": "default", **kwargs}
resp = client.post("/tasks/", json=payload, headers=headers)
assert resp.status_code == 201, f"Task creation failed: {resp.json()}"
return resp.json()
# ── CHUNK 2: Happy path ──────────────────────────────────────────
# WHAT: Verifies a user can list their own tasks.
# WHY: The most basic assertion — if this fails, nothing else matters.
def test_list_tasks_happy_path(client, auth_headers):
create_task(client, auth_headers, title="Buy groceries", priority="medium")
create_task(client, auth_headers, title="Write report", priority="high")
resp = client.get("/tasks/", headers=auth_headers)
assert resp.status_code == 200
tasks = resp.json()
assert isinstance(tasks, list)
assert len(tasks) >= 2
titles = [t["title"] for t in tasks]
assert "Buy groceries" in titles
assert "Write report" in titles
# ── CHUNK 3: Filter by completed ────────────────────────────────
# WHAT: Verifies ?completed=true only returns completed tasks.
# WHY: The filter uses SQLAlchemy boolean comparison — worth testing
# because Python True/False vs SQLite 1/0 can surprise you.
# GOTCHA: We create one completed and one incomplete task, then
# assert every returned item has completed=True.
def test_filter_completed(client, auth_headers):
create_task(client, auth_headers, title="Done task")
incomplete_id = create_task(client, auth_headers, title="Not done")["id"]
# Mark the first task done
done_id = client.get("/tasks/", headers=auth_headers).json()[0]["id"]
client.patch(f"/tasks/{done_id}", json={"completed": True}, headers=auth_headers)
resp = client.get("/tasks/?completed=true", headers=auth_headers)
assert resp.status_code == 200
assert all(t["completed"] is True for t in resp.json())
# The incomplete task should NOT appear
ids = [t["id"] for t in resp.json()]
assert incomplete_id not in ids
# ── CHUNK 4: Filter by priority ──────────────────────────────────
# WHAT: Verifies ?priority=low only returns low-priority tasks.
def test_filter_by_priority_low(client, auth_headers):
create_task(client, auth_headers, title="Low task", priority="low")
create_task(client, auth_headers, title="High task", priority="high")
resp = client.get("/tasks/?priority=low", headers=auth_headers)
assert resp.status_code == 200
tasks = resp.json()
assert len(tasks) >= 1
assert all(t["priority"] == "low" for t in tasks)
# ── CHUNK 5: Filter by tag ────────────────────────────────────────
# WHAT: Creates a tag, assigns it to a task, then filters by tag name.
# WHY: This exercises the JOIN between tasks and tags tables —
# the most complex query in list_tasks.
def test_filter_by_tag(client, auth_headers):
# Create a tag first
tag_resp = client.post("/tags/", json={"name": "urgent"}, headers=auth_headers)
assert tag_resp.status_code == 201
tag_id = tag_resp.json()["id"]
# Create a task with that tag
create_task(client, auth_headers, title="Tagged task", tag_ids=[tag_id])
create_task(client, auth_headers, title="Untagged task")
resp = client.get("/tasks/?tag=urgent", headers=auth_headers)
assert resp.status_code == 200
tasks = resp.json()
assert len(tasks) >= 1
# Every returned task should have the 'urgent' tag
for task in tasks:
tag_names = [tg["name"] for tg in task["tags"]]
assert "urgent" in tag_names
# ── CHUNK 6: Unauthenticated ──────────────────────────────────────
# WHAT: Verifies /tasks/ returns 401 with no token.
# WHY: Security regression test — must fail if auth is removed.
def test_list_tasks_unauthenticated(client):
resp = client.get("/tasks/")
assert resp.status_code == 401
# ── CHUNK 7: Invalid priority value ──────────────────────────────
# WHAT: Passes an unknown priority string — expects an empty list, not 422.
# WHY: priority is stored as a plain String in models.py, not an Enum.
# The filter just queries WHERE priority='urgent' and gets zero rows.
# This documents the current behavior explicitly.
def test_filter_invalid_priority_returns_empty(client, auth_headers):
create_task(client, auth_headers, title="Real task", priority="medium")
resp = client.get("/tasks/?priority=urgent", headers=auth_headers)
assert resp.status_code == 200
# No tasks have priority='urgent', so result is empty
assert resp.json() == []
What Just Happened?
Gemini read list_tasks in routers/tasks.py and saw 4 optional filter parameters. It read conftest.py and saw that auth_headers returns a dict you pass directly to headers=. It then generated 7 tests that cover every parameter combination — including the "what happens with invalid input?" case that most humans forget to write.
Edge Cases and Error Branches
Every error branch in your code is a hidden test requirement. Gemini can enumerate them before you even think to look.
Step 1: Ask Gemini to list untested error paths
gemini "@routers/tasks.py What error paths does this code have that are not yet covered by @tests/test_tasks.py?"
Gemini's response will resemble:
Looking at routers/tasks.py, I can identify the following error paths
that have no corresponding test in test_tasks.py:
1. create_task — tag_ids references a non-existent tag ID
Line 36: tags = db.query(models.Tag).filter(models.Tag.id.in_(task.tag_ids)).all()
If tag_id=9999 is passed, SQLAlchemy silently ignores it (no error raised).
Currently untested — behavior may be surprising to API consumers.
2. update_task — task belongs to a different user
Lines 73-78: The 404 guard only checks owner_id == current_user.id.
If user A tries to PATCH user B's task, they get 404 (correct).
Not tested — a security regression could silently change this.
3. delete_task — task belongs to a different user
Same pattern as update_task. Untested cross-user delete.
4. get_task — task exists but belongs to a different user
Line 58: Returns 404 even if task exists (by design). Not tested.
5. update_task — tag_ids list is empty []
Passing tag_ids=[] should clear all tags from a task.
This is a valid use case with no test coverage.
Recommendation: Write tests for items 2, 3, and 4 first — they are
security-relevant. Items 1 and 5 are behavioral edge cases.
Step 2: Generate the missing tests
gemini "@routers/tasks.py @tests/conftest.py Generate pytest tests for:
1. Updating another user's task returns 404
2. Deleting another user's task returns 404
3. Getting another user's task returns 404
4. Updating a task with tag_ids=[] clears all tags"
import pytest
# ── Helper: create a second user and return their auth headers ────
# WHAT: Registers a new user with a unique email/username and logs in.
# WHY: Cross-user isolation tests require two separate authenticated users.
@pytest.fixture()
def second_auth_headers(client):
client.post("/users/register", json={
"email": "other@example.com",
"username": "otheruser",
"password": "otherpass123",
})
resp = client.post("/users/login", data={
"username": "otheruser",
"password": "otherpass123",
})
return {"Authorization": f"Bearer {resp.json()['access_token']}"}
def test_cannot_update_other_users_task(client, auth_headers, second_auth_headers):
"""User A creates a task; User B should get 404 trying to PATCH it."""
create_resp = client.post(
"/tasks/", json={"title": "Private task"}, headers=auth_headers
)
task_id = create_resp.json()["id"]
# User B tries to update User A's task
resp = client.patch(
f"/tasks/{task_id}", json={"title": "Hijacked"}, headers=second_auth_headers
)
assert resp.status_code == 404
# Verify the original title is unchanged
verify = client.get(f"/tasks/{task_id}", headers=auth_headers)
assert verify.json()["title"] == "Private task"
def test_cannot_delete_other_users_task(client, auth_headers, second_auth_headers):
"""User A creates a task; User B should get 404 trying to DELETE it."""
create_resp = client.post(
"/tasks/", json={"title": "Do not delete"}, headers=auth_headers
)
task_id = create_resp.json()["id"]
resp = client.delete(f"/tasks/{task_id}", headers=second_auth_headers)
assert resp.status_code == 404
# Task should still exist for the owner
verify = client.get(f"/tasks/{task_id}", headers=auth_headers)
assert verify.status_code == 200
def test_cannot_read_other_users_task(client, auth_headers, second_auth_headers):
"""User A creates a task; User B should get 404 on GET /{id}."""
create_resp = client.post(
"/tasks/", json={"title": "Secret task"}, headers=auth_headers
)
task_id = create_resp.json()["id"]
resp = client.get(f"/tasks/{task_id}", headers=second_auth_headers)
assert resp.status_code == 404
def test_clear_tags_with_empty_list(client, auth_headers):
"""Passing tag_ids=[] on PATCH should remove all tags from a task."""
# Create a tag and a task that uses it
tag_resp = client.post("/tags/", json={"name": "clearme"}, headers=auth_headers)
tag_id = tag_resp.json()["id"]
create_resp = client.post(
"/tasks/", json={"title": "Tagged task", "tag_ids": [tag_id]}, headers=auth_headers
)
task_id = create_resp.json()["id"]
assert len(create_resp.json()["tags"]) == 1
# Now clear the tags
resp = client.patch(f"/tasks/{task_id}", json={"tag_ids": []}, headers=auth_headers)
assert resp.status_code == 200
assert resp.json()["tags"] == []
second_auth_headers fixture uses a client parameter, not auth_headers. This gives it a fresh authenticated session without depending on the first user — a clean pattern for multi-user isolation tests.What Just Happened?
Gemini identified 5 untested code paths and flagged the 3 security-relevant ones first. The generated tests each verify both the rejection (404 status) AND the side-effect (the original data is unchanged). This is a stronger assertion than just checking the status code.
Diagnose Failures by Pasting Output
The fastest debugging loop: run tests, paste the failure, ask Gemini to explain it. No context-switching, no Googling the error message.
The paste-and-diagnose pattern
Run your tests first:
cd sample-project/taskflow
pytest tests/ -v 2>&1 | Tee-Object -FilePath test-output.txt
Tee-Object sends output both to the terminal and to a file, so you can paste from the file. On bash: pytest tests/ -v | tee test-output.txtSuppose test_filter_by_priority_low fails with this output:
FAILED tests/test_list_tasks.py::test_filter_by_priority_low - AssertionError: assert 3 >= 1
...
E AssertionError: assert all(t["priority"] == "low" for t in tasks)
E + where tasks = [{'completed': False, 'id': 1, 'priority': 'high', 'title': 'High task', ...},
{'completed': False, 'id': 2, 'priority': 'low', 'title': 'Low task', ...},
{'completed': False, 'id': 3, 'priority': 'medium','title': 'Done task', ...}]
Paste that output directly to Gemini:
gemini "This pytest output shows test_filter_by_priority_low failing:
FAILED tests/test_list_tasks.py::test_filter_by_priority_low
AssertionError: assert all(t['priority'] == 'low' for t in tasks)
tasks = [high task, low task, medium task — all three returned]
Look at @tests/conftest.py and @tests/test_list_tasks.py and diagnose
why the filter is returning all tasks instead of just low-priority ones."
Gemini's diagnosis:
Root cause: Test database isolation problem.
The conftest.py uses a single SQLite file (test_taskflow.db) and the
db_session fixture does NOT roll back between tests. Tasks created by
earlier tests (test_create_task, test_list_tasks_happy_path) persist
in the database when test_filter_by_priority_low runs.
When you call GET /tasks/?priority=low, the query correctly filters by
priority — but the "High task" and "Medium task" from previous tests
are still in the database under the same user (auth_headers creates
the same testuser every time).
Fix: Add a database rollback or truncation between tests. The cleanest
approach is to use a transaction-per-test pattern:
In conftest.py, change db_session to:
@pytest.fixture()
def db_session():
connection = engine.connect()
transaction = connection.begin()
session = TestingSessionLocal(bind=connection)
try:
yield session
finally:
session.close()
transaction.rollback()
connection.close()
This wraps each test in a transaction that gets rolled back afterward,
so each test starts with a clean database.
list_tasks filter works correctly; the test just wasn't isolated. This is a common mistake that causes "flaky tests" — tests that pass in isolation but fail when run together.What Just Happened?
Gemini read conftest.py and immediately spotted that db_session doesn't roll back between tests. It gave you not just the diagnosis but a complete replacement fixture using the transaction-rollback pattern — the industry-standard approach for pytest + SQLAlchemy test isolation.
WITHOUT ROLLBACK (current)
WITH ROLLBACK (fixed)
Coverage Gap Analysis
Running pytest --cov gives you a precise map of which lines were never executed during your test run. Pasting that map to Gemini lets you skip the tedious "is this line important?" judgment call.
Run the coverage report
First, install the coverage plugin if you haven't:
pip install pytest-cov
cd sample-project/taskflow
pytest tests/ --cov=. --cov-report=term-missing --cov-omit="tests/*,conftest.py"
--cov=. measures coverage for all Python files in the current directory. --cov-report=term-missing shows which specific line numbers were not covered. --cov-omit excludes test files from the measurement (we care about app coverage, not test coverage of tests).Sample output:
----------- coverage: platform win32, python 3.12 -----------
Name Stmts Miss Cover Missing
-----------------------------------------------------
auth.py 26 11 58% 36-44, 47, 53-54
database.py 10 1 90% 27
main.py 8 0 100%
models.py 17 0 100%
routers/tags.py 18 8 56% 17, 22-29, 32-37
routers/tasks.py 45 12 73% 36-37, 57-62, 81-82, 99-106
routers/users.py 22 9 59% 15-16, 19-22, 35-37, 40-42
schemas.py 0 0 100%
-----------------------------------------------------
TOTAL 146 41 72%
Now paste it to Gemini:
gemini "Here's my TaskFlow coverage report:
auth.py 58% Missing: 36-44, 47, 53-54
routers/tags.py 56% Missing: 17, 22-29, 32-37
Look at @auth.py and @routers/tags.py. Which uncovered lines are most
important to test from a security perspective? Write pytest tests for
the top 3 gaps."
Gemini will identify lines 36-44 in auth.py as the highest priority — those are the JWT decode/verify path in get_current_user — and generate tests for malformed tokens, expired tokens, and valid tokens for deleted users.
Checkpoint
After this section you should have: the pytest --cov output for TaskFlow, the specific line numbers that are uncovered, and a Gemini-generated list of which gaps are security-critical. Keep this output — the lab in Section 8 starts from here.
Testing the Auth Flow
Authentication code is the highest-value thing to test. A bug in get_current_user can leak every user's data. Yet it's often the least-tested module because the happy path (valid token → user object) is trivially easy to write, and the failure paths require crafting malformed JWTs by hand.
Gemini removes that friction.
JWT edge cases prompt
gemini "@auth.py @tests/conftest.py @routers/users.py Generate pytest tests for:
1. Registration with a duplicate email returns 400
2. Registration with a duplicate username returns 400
3. Login with wrong password returns 401
4. Accessing /tasks/ with a completely fake (not signed) JWT token returns 401
5. Accessing /tasks/ with a valid token for a user that no longer exists returns 401"
import pytest
from datetime import timedelta
from auth import create_access_token
# ── CHUNK 1: Registration errors ─────────────────────────────────
# WHAT: Tests that the uniqueness constraints on email/username work.
# WHY: routers/users.py lines 15-18 — these are the "Email already
# registered" and "Username already taken" branches.
def test_register_duplicate_email(client):
payload = {"email": "dup@example.com", "username": "user1", "password": "pass123"}
resp1 = client.post("/users/register", json=payload)
assert resp1.status_code == 201
# Same email, different username
resp2 = client.post("/users/register", json={
"email": "dup@example.com", "username": "user2", "password": "pass456"
})
assert resp2.status_code == 400
assert "Email already registered" in resp2.json()["detail"]
def test_register_duplicate_username(client):
client.post("/users/register", json={
"email": "a@example.com", "username": "dupeuser", "password": "pass123"
})
resp = client.post("/users/register", json={
"email": "b@example.com", "username": "dupeuser", "password": "pass456"
})
assert resp.status_code == 400
assert "Username already taken" in resp.json()["detail"]
# ── CHUNK 2: Login failure ────────────────────────────────────────
# WHAT: Tests that wrong credentials return 401.
# WHY: auth.py verify_password — tests the bcrypt comparison branch
# where the hash doesn't match.
def test_login_wrong_password(client):
client.post("/users/register", json={
"email": "real@example.com", "username": "realuser", "password": "correctpass"
})
resp = client.post("/users/login", data={
"username": "realuser",
"password": "wrongpass"
})
assert resp.status_code == 401
assert "Incorrect username or password" in resp.json()["detail"]
# ── CHUNK 3: Fake JWT ─────────────────────────────────────────────
# WHAT: Sends a completely forged (not signed by our SECRET_KEY) JWT.
# WHY: Tests auth.py lines 46-48 — the JWTError catch block.
# GOTCHA: We use a hardcoded fake token. Any string that is not a
# valid HS256 JWT signed by SECRET_KEY will trigger JWTError.
def test_fake_jwt_rejected(client):
fake_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.fake.signature"
resp = client.get("/tasks/", headers={"Authorization": f"Bearer {fake_token}"})
assert resp.status_code == 401
# ── CHUNK 4: Valid token for deleted user ─────────────────────────
# WHAT: Creates a user, logs them in, deletes the user from the DB,
# then tries to use the token.
# WHY: auth.py lines 53-55 — the "user is None" branch after
# successful JWT decode. This is the "revoked credential" scenario.
# GOTCHA: We need direct DB access to delete the user without going
# through the API (there's no DELETE /users endpoint).
def test_valid_token_deleted_user(client, db_session):
import models
# Register and log in
client.post("/users/register", json={
"email": "gone@example.com", "username": "goneuser", "password": "pass123"
})
login_resp = client.post("/users/login", data={
"username": "goneuser", "password": "pass123"
})
token = login_resp.json()["access_token"]
# Delete the user directly from the test database
db_session.query(models.User).filter(
models.User.username == "goneuser"
).delete()
db_session.commit()
# Token is technically valid (correct signature, not expired)
# but the user no longer exists
resp = client.get("/tasks/", headers={"Authorization": f"Bearer {token}"})
assert resp.status_code == 401
Mutation Testing
A mutation is a deliberate, small change to your production code — flipping == to !=, changing completed == True to completed == False, removing an if not task guard. A mutation testing framework makes hundreds of these changes, runs your test suite after each one, and reports how many mutations your tests "killed" (caused a test to fail). A mutation that survives means your tests would not catch that bug.
Think of it as testing your tests — not just "does this line run?" but "if this line were wrong, would we know?"
You don't need a mutation framework to get value from this concept. Ask Gemini to reason about specific mutations:
gemini "@tests/test_tasks.py @routers/tasks.py
Suppose a developer accidentally inverted the completed filter:
query = query.filter(models.Task.completed != completed)
instead of
query = query.filter(models.Task.completed == completed)
Would any of the current tests catch this bug? If not, write a
mutation test that verifies the filter direction is correct."
Gemini's response:
Looking at the existing tests: test_filter_by_priority checks the priority
filter but there is no test for the completed filter. The original
test_list_tasks_empty just checks that the list endpoint returns a list.
With the inverted filter (completed != completed), a query for
?completed=true would return incomplete tasks, and vice versa.
NONE of the current 8 tests would catch this mutation.
Here is a mutation test that will catch it:
def test_completed_filter_direction(client, auth_headers):
"""Verifies the completed filter returns the RIGHT tasks, not the inverse."""
# Create one completed and one incomplete task
resp1 = client.post("/tasks/", json={"title": "Done"}, headers=auth_headers)
done_id = resp1.json()["id"]
resp2 = client.post("/tasks/", json={"title": "Not done"}, headers=auth_headers)
pending_id = resp2.json()["id"]
client.patch(f"/tasks/{done_id}", json={"completed": True}, headers=auth_headers)
# Filter for completed=true — should contain done_id, NOT pending_id
completed = client.get("/tasks/?completed=true", headers=auth_headers).json()
completed_ids = [t["id"] for t in completed]
assert done_id in completed_ids, "Completed task must appear in ?completed=true"
assert pending_id not in completed_ids, "Pending task must NOT appear in ?completed=true"
# Filter for completed=false — should contain pending_id, NOT done_id
pending = client.get("/tasks/?completed=false", headers=auth_headers).json()
pending_ids = [t["id"] for t in pending]
assert pending_id in pending_ids, "Pending task must appear in ?completed=false"
assert done_id not in pending_ids, "Completed task must NOT appear in ?completed=false"
assert X not in) is what catches the inversion.Lab: Achieve 90% Coverage
This lab walks you through the full cycle: measure → identify → generate → run → repeat. Expected time: 30–40 minutes. Expected final coverage: 90%+.
-
Navigate to the project:
PowerShell
cd sample-project/taskflow pip install -r requirements.txt pip install pytest-cov -
Run the baseline coverage report:
PowerShell
pytest tests/ --cov=. --cov-report=term-missing --cov-omit="tests/*" -qExpected: ~65-72% overall, with auth.py and routers/users.py showing the largest gaps.
-
Ask Gemini for a prioritized gap list:
PowerShell / bash
gemini "Here is my pytest --cov output. Which 5 gaps should I fill first to reach 90% coverage with the fewest tests? [paste your coverage output here] Look at @auth.py @routers/tasks.py @routers/users.py @routers/tags.py" -
Generate tests for the top 5 gaps:
PowerShell / bash
gemini "@auth.py @routers/users.py @tests/conftest.py Generate pytest tests for auth.py lines 36-44 (JWTError path), auth.py line 47 (username is None), auth.py lines 53-54 (user not found), and routers/users.py lines 15-16 (duplicate email) and 19-22 (duplicate username)" -
Save to a new test file and run:
PowerShell
# Save Gemini's output to tests/test_auth_coverage.py # Then run: pytest tests/ --cov=. --cov-report=term-missing --cov-omit="tests/*" -qExpected: coverage climbs to ~80-85%.
-
Generate tests for routers/tags.py:
PowerShell / bash
gemini "@routers/tags.py @tests/conftest.py Generate pytest tests for: list tags (happy path), create tag, create duplicate tag (400), delete tag, delete non-existent tag (404)" -
Final run — target 90%:
PowerShell
pytest tests/ --cov=. --cov-report=term-missing --cov-omit="tests/*"Expected terminal output at this stage:
Expected outputName Stmts Miss Cover ------------------------------------------- auth.py 26 2 92% database.py 10 1 90% main.py 8 0 100% models.py 17 0 100% routers/tags.py 18 1 94% routers/tasks.py 45 3 93% routers/users.py 22 1 95% schemas.py 0 0 100% ------------------------------------------- TOTAL 146 8 95% 28 passed in 3.41s
28 tests, 95% coverage, 3 seconds. You went from 8 hand-written tests covering 72% of the code to 28 Gemini-assisted tests covering 95% — in roughly 40 minutes of focused prompting. The remaining 5% is typically defensive code that's very hard to trigger in a unit test context (like the database session close in a finally block).
Knowledge Check
Five questions. Each tests a concept from this module applied specifically to TaskFlow.
1. In TaskFlow's conftest.py, what does the auth_headers fixture return?
TestClient instance pre-configured with authentication
{"Authorization": "Bearer <token>"} that can be passed to the headers= parameter
models.User object for the test user
2. You run pytest tests/ --cov=. --cov-report=term-missing and see auth.py 58% Missing: 36-44. What is the most efficient Gemini CLI prompt to address this?
gemini "Write tests for auth.py"
gemini "@auth.py @tests/conftest.py Generate tests for lines 36-44 — explain what code those lines are before writing the tests"
gemini "Improve my test coverage"
3. The test_filter_by_priority_low test fails when run in a suite even though it passes alone. Based on what you learned about TaskFlow's conftest, what is the most likely cause?
list_tasks endpoint has a bug in the priority filter
db_session doesn't roll back between tests
second_auth_headers
4. Which of the following mutations in routers/tasks.py would be caught by the test test_cannot_update_other_users_task?
order_by(models.Task.created_at.desc()) to order_by(models.Task.id)
models.Task.owner_id == current_user.id filter from the update query
description field to the default priority value
/tasks to /task
5. The test_valid_token_deleted_user test deletes a user directly using db_session rather than calling a DELETE endpoint. Why?
DELETE /users/{id} endpoint, so the test must use the database directly to simulate account deletion
auth_headers fixture doesn't allow re-authentication after deletion