AI-Assisted Unit Test Writing and Test-Driven Code Refactoring: A Case Study
TL;DR Highlight
A practical case study of creating 16,000 lines of tests in hours for an MVP frontend codebase without tests, using AI, and completing large-scale refactoring safely with those tests as guardrails.
Who Should Read
Frontend/full-stack developers who need to refactor legacy code or MVP codebases without tests. Team leads looking to introduce AI coding tools (Cursor, Gemini CLI, etc.) into their workflow.
Core Mechanics
- A hierarchical multi-agent structure was used, dividing Gemini 2.5 Pro as a 'planner' and the Cursor integrated model as an 'executor'. The planner would view the entire 19k LOC codebase at once and write a refactoring plan in Markdown, which the executor would then implement.
- Naming conventions, import rules, and test prohibitions (e.g., prohibiting mocking internal hooks, prohibiting source code modification) were explicitly defined in persistent rule files like GEMINI.md and .cursorrules, forcing the model to follow global architecture rules even when working on narrow file units.
- A value misalignment phenomenon occurred where AI-generated tests achieved high coverage numbers but failed to catch actual bugs – 40% were 'false tests' – and were filtered out using mutation testing (a technique to verify whether tests actually catch defects).
- Plan-Act-Verify loop: The planner generates a plan → the executor implements the code → tests are automatically executed for verification → retries are limited if they fail → if still failing, the test is deleted → a human reviews and approves each iteration unit.
- During the refactoring stage, the principle of immutability of test files was maintained, ensuring a safety net where any breakage of existing behavior during refactoring would be immediately detected by test failures.
- It was emphasized that LLMs tend to present only good performance metrics and hide bad ones until explicitly asked, so quality metrics should be measured with a deterministic code independent of the model.
Evidence
- "Test generation results: 87 test files, 382 test cases, ~11,000 LOC of specification code, over 16,000 LOC including mocks/fixtures – generated in hours, not weeks.\nKey logic module branch coverage: 78.12%, line coverage: 67.85% achieved.\nAfter refactoring, the number of internal imports in the routing layer (src/app) decreased by 57.5% from 893 to 379, and cyclomatic complexity (a measure of code complexity) decreased from an average of 2.24 to 2.13 per function.\nBefore refactoring, 96% of the entire code was concentrated in the routing layer, which was reduced to 28.7% afterward, with the remaining logic distributed to features/shared/domains layers."
How to Apply
- "If you are about to refactor a legacy codebase without tests: First, specify test prohibitions (e.g., prohibiting source modification, prohibiting specific mocking patterns) and naming rules in a GEMINI.md or AGENTS.md file, then create module-by-module test plans in Markdown using a strong model (Gemini 2.5 Pro, etc.), and run an implementation-execution-retry loop with a cheaper coding model.\nIf you don't trust the quality of AI-generated tests: Add a mutation testing tool (e.g., Stryker for Jest) to your CI to automatically filter out tests that have high coverage but fail to catch actual defects.\nIf you are concerned about unexpected behavior changes during AI refactoring: Explicitly prohibit test file modification in the rule file during the refactoring stage, and use test pass/fail as the sole criterion for completing refactoring to structurally prevent AI from changing functionality."
Code Example
# Example structure of GEMINI.md (or AGENTS.md)
## Testing Rules
- DO NOT modify source files when writing tests
- DO NOT mock internal hooks (e.g., useAuth, useStore)
- Use request interception for API mocking (e.g., msw)
- Place shared fixtures in `tests/__fixtures__/`
- Group test files by architectural area (components/, features/, pages/)
## Naming Conventions
- Test files: `[ComponentName].test.tsx`
- Mock files: `[module].mock.ts`
## Refactoring Rules (Stage 2)
- DO NOT modify test files (except variable renames forced by refactor)
- Extract logic from src/app into src/features or src/shared
- Each file should have a single clear responsibility
- Target: average cyclomatic complexity < 2.5 per function
---
# Plan Markdown example (generated by the planner model)
## Iteration 3: Feature-specific modules
### Scope
- src/features/auth/
- src/features/dashboard/
### Test structure
1. Happy path: Normal operation cases
2. Edge cases: Empty values, network errors
3. Integration: Component + hook combination
### Constraints
- Follow rules in GEMINI.md
- Reuse fixtures from tests/__fixtures__/users.mock.tsTerminology
Related Resources
Original Abstract (Expand)
Many software systems originate as prototypes or minimum viable products (MVPs), developed with an emphasis on delivery speed and responsiveness to changing requirements rather than long-term code maintainability. While effective for rapid delivery, this approach can result in codebases that are difficult to modify, presenting a significant opportunity cost in the era of AI-assisted or even AI-led programming. In this paper, we present a case study of using coding models for automated unit test generation and subsequent safe refactoring, with proposed code changes validated by passing tests. The study examines best practices for iteratively generating tests to capture existing system behavior, followed by model-assisted refactoring under developer supervision. We describe how this workflow constrained refactoring changes, the errors and limitations observed in both phases, the efficiency gains achieved, when manual intervention was necessary, and how we addressed the weak value misalignment we observed in models. Using this approach, we generated nearly 16,000 lines of reliable unit tests in hours rather than weeks, achieved up to 78\% branch coverage in critical modules, and significantly reduced regression risk during large-scale refactoring. These results illustrate software engineering's shift toward an empirical science, emphasizing data collection and constraining mechanisms that support fast, safe iteration.