A refactor replaces an if/elif chain with a dictionary lookup. The tests pass and the diff looks tidy. A week after the merge, a caller that expects ValueError crashes on an uncaught KeyError. The suite was green, but the public behavior changed. Documentation and tests can hide the same kind of gap.
The document–test–refactor loop
Keep Copilot inside a loop you control. Read the current behavior, write a behavior contract, request a plan or draft, inspect the response and diff, then run a test or direct check. Revise when the evidence identifies a problem. The behavior contract states what callers can observe and rely on, including accepted inputs, return values, errors, side effects, and important boundaries. "Given a positive weight and a supported destination, return the fee rounded to two decimals. Reject non-positive weights and unsupported destinations with ValueError" gives Copilot constraints and gives you acceptance criteria in one sentence.
A request made without that contract is open-ended, and so is the review that follows it. Once it is written down, documentation has facts to check against, tests have behavior to assert, and a refactor has something to hold steady. All three tasks in this lesson run on that one discipline, pointed at a different artifact each time.
Order matters here. Read the current behavior, write the contract, then ask Copilot for something. Jump straight to "write tests for this" and you hand Copilot an open-ended task, then grade its output against a standard you never wrote down. Thirty seconds spent stating what a caller can observe gives every later step something to measure against.
What makes documentation trustworthy?
Useful documentation describes observable behavior. "Returns the fee rounded to two decimal places" remains true after the calculation is reorganized. "Adds base and surcharge" describes today's implementation and can become stale after a refactor. Ask for what a caller needs: purpose, parameters and types, return value, exceptions, side effects, boundaries, and one usage example.
The real risk with generated docs is that they turn guesses into guarantees. Copilot will document an exception the function never raises or an ordering the code never promises, and a future caller will trust it. The defense is a rule you state in the prompt and enforce in review: use only behavior established by the code, and label anything uncertain as TODO rather than filling the gap. Then check every claimed parameter, exception, side effect, and example against the implementation before you keep it. Documentation you didn't verify is a confident guess with better formatting.
Mock the external boundary
A unit test isolates one function and controls its dependencies. An integration test proves that two or more real components agree on a protocol, schema, or format. Choose by asking what behavior you're verifying. Calculation, validation, or error translation inside one function? Unit test. Whether a real database adapter and your code agree on a query result? Integration test, because mocking the dependency there would hide the very thing you need to check.
Derive cases from behavior categories instead of implementation branches. Cover a normal input, the exact boundary, values just inside and outside it, and an invalid input the code should reject. When a dependency exists, include its success and failure paths. A generated test matrix can draft this quickly. Remove any row that assumes behavior the contract never established before you turn the matrix into code.
When you do write a unit test, mock the external boundary (an HTTP client, a database, a clock, an email service) and nothing else. Mocking the business logic under test is how you get a green suite that proves nothing. Aim every assertion at observable behavior: the returned value, the public error, or a necessary call to a collaborator. Asserting a private helper name or an incidental order of operations couples your test to the implementation, so it breaks on a harmless refactor and passes on a real regression.
A fixture is fine for reusable setup, but keep the scenario's inputs and expected results visible in the test. A fixture that hides the expected value hides the point of the test.
A worked case makes the boundaries concrete. Say a checkout function calls a tax client and translates a network timeout into a domain error like QuoteUnavailableError. The unit test mocks that client, returning a fixed rate in one case and raising a timeout in another, then asserts the returned total in the first and the public domain error in the second. It never makes a real network request, and it never inspects a private variable. It checks exactly what a caller would observe. Reserve mocks for boundaries that are external, destructive, slow, or nondeterministic. A pure calculation like a shipping surcharge needs real input values, not a mock.
Refactors need a baseline
Refactoring changes internal structure while keeping externally observable behavior identical: naming a complex expression, extracting a helper, replacing repeated conditionals with a lookup. Change a signature, a return value, an exception type, a rounding rule, an evaluation order, or a side effect and you have changed behavior, whatever the commit message calls it.
A refactor needs a baseline. Run the existing tests, or record a few direct calls and their results, before you change the code. Repeat the same checks afterward. Read the diff itself. Look specifically for changes to public signatures, return values or types, exception types and messages, validation order, rounding, ordering, and dependency calls.
The dictionary-lookup example makes the trap concrete. Turning if destination == "domestic" … elif … else raise ValueError into base = base_fees[destination] is cleaner, but a bare lookup exposes KeyError for an unknown destination, a caller-visible change from the old ValueError. The behavior-preserving version catches the lookup failure and re-raises the existing ValueError with its original message, using from None so the internal KeyError doesn't leak as chained context. Same structure improvement, same public contract.
The evidence that you got it right is the same baseline run passing before and after, including a test for the unsupported destination, the case a happy-path suite leaves out. Copilot drafts this quickly. Checking it is still your job, and you carry that into the next lesson, Privacy, Plans, and the GH-300 Path.