Refactoring, Test Generation & Debugging
G03 taught the conversation tools. This module points them at the two chores developers postpone forever — refactoring and writing tests — and shows how to delegate the labor while keeping the judgment.
Code Transformations
A house renovation has two distinct jobs: the architect decides "this wall should move," and the crew swings the hammers. Most developers are forced to be both — they know the function should take a config object instead of seven positional arguments, but actually threading that change through every call site is an afternoon of careful drudgery. So the wall never moves, and the codebase slowly becomes a maze. Code transformations split the jobs back apart: you stay the architect ("extract this into smaller functions, add type hints, handle the error cases"), and Gemini becomes the crew — with the crucial twist that you inspect every swing of the hammer as a diff before it touches the house.
Code transformations are edits requested in natural language against existing code: select code, then ask in chat (or via right-click smart actions) for things like:
- Restructuring: "extract the validation into its own function", "convert this class to a dataclass"
- Hardening: "add error handling for network failures", "add type hints everywhere"
- Modernizing: "rewrite for Python 3.12 idioms", "migrate from requests to httpx"
- Renaming & readability: "rename these variables to describe their purpose"
In current VS Code and IntelliJ builds, chat proposes changes in a preview/diff view — added lines green, removed lines red — that you apply or reject. The behavioral rule for all refactoring applies doubly with AI: behavior stays identical; only structure improves. Tests are how you prove it, which is why this module teaches both together.
Unit Test Generation
Writing tests for your own code is like proofreading your own essay: you read what you meant, not what you wrote, so your blind spots survive. A fresh reader catches what you can't — not because they're smarter, but because they don't share your assumptions. Generated tests are that fresh reader at scale: the model reads your function's actual contract (signature, docstring, branches) and probes it, including the negative paths you mentally skipped — empty lists, negative numbers, the exception you declared but never tested.
Select a function/class → right-click → Gemini Code Assist → Generate unit tests (or ask in chat). Gemini infers the testing framework from your project (pytest if it sees pytest, JUnit in Java projects, Jest in JS) and produces test cases covering happy paths, edge cases, and declared exceptions. Generated tests are proposals: they encode the model's reading of your contract, and where that reading differs from your intent, you've found either a test to fix or — more interestingly — a bug or unclear API in your code.
Teams that "never have time for tests" are the exact teams drowning in regressions that eat the time they saved. Test generation drops the cost of a first test suite from days to minutes — Recipe Box's storage layer gets ~10 meaningful tests in this lab in under 15 minutes. Once the suite exists, every future refactor (and every agent-mode change in G06) has a safety net, and the net was nearly free.
Lab Part 1: Refactor Legacy Search Code
A "previous developer" left Recipe Box a working-but-awful search function. Your job: make it clean without changing behavior.
Add the legacy code
Create search.py and paste:
import storage
def s(q, mx, f):
r = storage.list_recipes()
o = []
for x in r:
ok = 0
if q != None and q != "":
if q.lower() in x["name"].lower():
ok = 1
else:
for i in x["ingredients"]:
if q.lower() in i.lower():
ok = 1
else:
ok = 1
if ok == 1:
if f == 1:
if x["minutes"] <= 30:
o.append(x)
else:
o.append(x)
if mx != None and mx > 0:
o = o[0:mx]
return o
f == 1 means ≤30 minutes), optional result cap. WHY it's painful: single-letter names, flag-as-int, != None, five levels of nesting. It works — that's the point. Refactoring is for working code.Understand before you touch (the professional's rule)
Select the whole function and run /explain. Read the answer and confirm it matches the annotation above. Never refactor code you can't describe — with or without AI.
Direct the transformation
With the function still selected, give the architect's instruction in chat:
Refactor this function. Keep behavior identical. Requirements:
1. Rename it search_recipes with descriptive parameter names
(query, max_results, quick_only) and full type hints; quick_only is a bool.
2. Extract the "does this recipe match the query" logic into a
helper function _matches(recipe, query).
3. Flatten the nesting with early returns / continue.
4. Add a Google-style docstring stating the quick_only threshold (30 minutes).
max_results=None or ≤0 means unlimited.) If the proposal "improved" any behavior — e.g., made empty query return nothing — reject that hunk and say so: "keep the empty-query-matches-all behavior."Prove behavior survived
Append and run a quick harness (your G02 data is still in recipes.db):
if __name__ == "__main__":
print(len(search_recipes(None, None, False))) # all recipes
print(len(search_recipes("chai", None, False))) # name match: 1
print(len(search_recipes("cardamom", None, False)))# ingredient match: 1
print(len(search_recipes("pizza", None, False))) # no match: 0
print(len(search_recipes(None, None, True))) # only quick (<=30 min)
Lab Part 2: Generate a Real Test Suite
Install pytest and generate
pip install pytest
Open storage.py, select all of it, right-click → Gemini Code Assist → Generate unit tests (or in chat: "Generate pytest tests for this module, covering init_db, add_recipe, list_recipes, including StorageError paths and the minutes > 0 constraint"). Save the result as tests/test_storage.py.
Run them — and expect trouble
pytest tests/ -v
Generated tests often fail at first — and the cause is usually a testability flaw in your code, not in the tests. Here: storage.py hardcodes DB_PATH = "recipes.db", so tests pollute (and are polluted by) your real database. The generated suite just exposed a design smell you'd have hit eventually in production.
Fix the design, not the symptom
Ask Gemini to make it right. In chat, with tests/test_storage.py open and storage.py in another tab:
These tests fail because storage.py hardcodes DB_PATH and all tests share my
real recipes.db. Write a pytest fixture (conftest.py) that points storage at a
temporary database per test using tmp_path and monkeypatch, and update the
tests to use it.
The expected shape of the fix:
import pytest
import storage
@pytest.fixture(autouse=True)
def temp_db(tmp_path, monkeypatch):
"""Point storage at a fresh database file for every test."""
monkeypatch.setattr(storage, "DB_PATH", str(tmp_path / "test.db"))
storage.init_db()
yield
pytest tests/ -v now shows all green — typically 8–12 passing tests covering inserts, listing order, JSON round-tripping of ingredients, the minutes > 0 constraint, and StorageError wrapping. Commit this suite mentally: G06's agent will be graded against it.Part 1: you acted as architect and reviewed the crew's diff — behavior preserved, structure transformed. Part 2: generated tests failed usefully, exposing a real testability flaw (hardcoded DB path), and the fix — a standard pytest fixture — made both the tests and the module better. This is the mature pattern: AI output failures are often information about your code, not noise.
The Debugging Workflow, Assembled
You've now used every piece. Here's the loop to internalize, in order of escalation:
- Reproduce — get the exact error text or wrong output. (No evidence, no debugging.)
- /explain the suspect code — confirm what it actually does vs. what you assumed. A surprising number of bugs die right here.
- /fix with evidence — selection + verbatim error + expected behavior.
- Apply via diff, then re-run — the failing test or curl from step 1 is your referee.
- Lock it in with a test — "write a pytest test that would have caught this bug." Now the bug can never return silently.
Step 5 is the compounding one. A fixed bug saves you once; a regression test for it saves you every release forever. Since generated tests cost minutes, the old excuse ("no time to write a test for every fix") is gone — and a codebase where every past bug has a guard test is the codebase you can safely hand to agent mode next module.
Knowledge Check
1. The golden rule of refactoring (AI-assisted or not) is…
2. In the lab, the generated tests failed at first because…
3. Before applying a refactoring diff, the single most important check is…
4. What did the conftest.py fixture accomplish?
5. After fixing any bug, the compounding final step is…
Quiz Complete!
Ready for G05: Context Mastery — GEMINI.md & .aiexclude →