In this article
Before you start
This page belongs to the machine coding rounds guide to industry coding, where the timed, extend-it-as-requirements-change format is explained.
Why interviewers ask this
A round that hands you existing code tests a different skill from a round that starts from a blank file. Candidates report prompts such as refactoring a messy, deeply nested block to support a new requirement while the existing tests keep passing, and a to-do checklist that allows inserting items anywhere, followed by a nesting follow-up. These are reports from candidates, not public statements by any company.
The consequence of getting the order wrong is concrete: a candidate who rewrites first and tests later cannot tell whether a failing test is a new bug or an old behavior that was lost. A weak answer starts typing a cleaner version. A strong answer says what must not change, shows that it does not change, and only then adds the new requirement. A Staff-level answer also keeps the structural change and the behavioral change in separate steps, so each can be undone alone.
Refactoring and extending code under tests from first principles
- Refactoring
- Refactoring is a disciplined technique that restructures an existing body of code, changing its internal structure without changing its externally observable behavior, by applying a series of small behavior-preserving steps and keeping the system working after each one.
- Characterization test
- A characterization test is a test that records what a piece of code does today, including its quirks, rather than what it ought to do, so that any later change in behavior makes a test fail.
- Legacy code
- Legacy code is code that has no tests protecting it, in Michael Feathers's sense; its age does not matter, only whether a test tells you when you broke it.
In plain words
Think of renovating a kitchen while a family still eats in it. You do not knock down every wall in one go. You move one cupboard, check the sink still runs, then move the next. The analogy breaks in one place: a kitchen has no automatic checker, while code can have one, and that checker is the test suite.
In one precise sentence: you change the shape of the code in tiny steps. A failing test then points at the last step. You change what the code does only after the shape is ready.
Refactoring is defined by Martin Fowler as a technique whose heart is "a series of small behavior preserving transformations". It is one specific technique, not another word for cleaning up. Feathers describes legacy code as code without tests. The reason is practical. With tests you can change behavior quickly and verifiably. Without them you cannot tell whether the code is getting better or worse.
Step 1: Pin today's behavior with characterization tests
Pinning behavior means writing tests against the code as it is, before changing one line. The input is the existing code and your reading of it; the output is a set of passing tests that describe what it does today.
Start with the public methods the rest of the program calls. For each one, run a normal case, then the edges: an empty collection, an index past the end, a negative index. Write the result you observe as the expected value, even where it surprises you. A surprise is not yours to fix now; it is behavior someone may rely on. The University College London (UCL) software course puts the rule plainly: have a regression test that covers the behavior in place before refactoring, because badly structured code cannot be unit tested when there are no units.
If the interviewer says tests already exist, read them first and add a characterization test only where they leave a quirk uncovered. You now have a safety net; the next step is to use it.
Step 2: Refactor in small steps, running the tests after each one
Each refactoring step changes structure and nothing else. The input is passing code; the output is passing code with a clearer shape. The UCL course advises making changes as small, reversible, incremental steps to reach the target design more reliably, and Fowler adds that the system is kept fully working after each refactoring.
Typical small moves are: extract a small class for a record you pass around as a bare list, replace a ladder of nested conditionals with one clamp or guard, rename a variable so it states its meaning, and move a block into a helper method. After each move, run the whole suite. If a test fails, undo that one move; you never debug a pile of changes.
Keep the public method names and return values identical in this step. Renaming a method is still a refactoring when you update every caller, but in a round the hidden tests and other code are callers you cannot see, so treat public names as a published interface and leave them alone.
Step 3: Write a failing test for the new requirement
Test-driven development (TDD) is a technique in which the writing of automated unit tests drives the creation of production code. Fowler's three steps, as quoted by a vendor explainer, are: write a test for the next bit of functionality, write the code until it passes, then refactor both new and old code.
Here you apply only the first of those. Take the new requirement, such as nested items, and write one test that describes it. Run it and watch it fail, for the right reason: a missing parameter or a wrong render, not a typo in the test. The failing test is the requirement written as code, and it tells you when you are done.
A reader's notes on Fowler's book attribute this rule to him: when a program is not structured for a new feature, first refactor to make the feature easy, then add the feature. The order is the point: restructure first, then extend.
Step 4: Make the smallest change that turns the test green
The input is one failing test; the output is the smallest code that passes it, with every old test still passing. Resist adding the second feature, a clever option or a speculative parameter. If the nesting test needs a parent argument, add exactly that.
Make the new argument optional with a default that reproduces the old behavior, so every existing call keeps working. This is why Step 2 came first: the clean structure from the refactor is what makes the smallest change small. Run the full suite, old and new tests together.
If an old test fails now, you changed behavior while adding the feature. Either the feature really requires the old behavior to change, in which case you state that out loud to the interviewer and update that test on purpose, or your change was careless and you undo it.
Step 5: Refactor again and run the full suite
The last step restores the shape. Adding a feature often leaves a duplicated loop or an awkward name behind. With all tests green, tidy the new code the same way as in Step 2: small moves, suite after each.
Stop when the code reads clearly enough to explain in a sentence. In a timed round, this is also the step to cut when time is short, because the code already works. Say what you would tidy next instead of risking a late break.
You now hold the full routine: pin, restructure, state the new requirement as a failing test, make it pass, tidy. The diagram below runs through the five steps once.
Step-by-step description
- Characterization tests record what the code does today.
- Small structural steps, the suite runs after each.
- The new requirement is written as a test that fails.
- The smallest change turns the new test green.
- Refactor again and run the full suite.
- Pin current behavior with tests before any change.
- Refactor in small steps with every test green.
- Write the failing test for the new feature before the feature code.
- Refactor again and run the full suite.
Knowing it is half the job. Saying it is the other half.
Interview Clozo is an AI interview copilot for real interview rounds: it hears the question and puts a strong answer on your screen, and you say it in your own words. See the features or how it works.
Start Free Trial →Worked example
The ledger's own example is a to-do checklist that lets users insert items anywhere, with nesting as the follow-up. The code here is illustrative and small, and it was run: the script todo-refactor.py holds the full classes and the tests.1
The starting code is a messy Checklist with nested conditionals in add:
def add(self, text, pos=None):
if pos is None:
self.items.append([text, False])
else:
if pos < 0:
self.items.insert(0, [text, False]) # quirk: negative means front
else:
if pos > len(self.items):
self.items.append([text, False]) # quirk: too big means end
else:
self.items.insert(pos, [text, False])Step 1 pins six characterization tests: append order, insert in the middle, a position past the end, a negative position, toggling (including a bad index), and rendering an empty list. All six pass on the original.1
Step 2 extracts an Item class and replaces the ladder with one clamp. All six still pass. Now the trap: a candidate "simplifies" by calling self.items.insert(pos, ...) directly, because Python's insert already handles a position past the end. It does not treat a negative position the same way, so the negative-position test fails and five of six pass.1 The tests found a behavior change the reader could not see.
Steps 3 and 4 add five nesting tests. Before any new code, all five fail; after adding an optional parent path, all five pass and the old six still pass, eleven of eleven.1
| Stage | Tests run | Tests passing |
|---|---|---|
| Original code, characterized | 6 | 6 |
| After the refactor | 6 | 6 |
| After the one-line "simplification" | 6 | 5 |
| New nesting tests, before the feature | 5 | 0 |
| After nesting is added, all tests | 11 | 11 |
The final render of two top-level items, with two children under the first, shows the new behavior: [ ] Shop, then an indented [ ] eggs and [ ] milk, then [ ] Cook.1
Text description of the diagram
- Six of six tests pass on the original and after the refactor.
- Five of six tests pass after the bad one-line fix.
- Eleven of eleven tests pass after nesting is added.
Trade-offs and edge cases
Small safe steps cost more keystrokes than a rewrite, and they pay back when something breaks. Small safe steps check behavior after every step, while a rewrite checks behavior only at the end, so the first break shows up right next to the change that caused it. A patch on top of the mess adds the new feature without restructuring; it is fast for a one-line change but mixes new bugs with old ones.
Text description of the diagram
- Small safe steps check behavior after every step.
- A rewrite checks behavior only at the end.
Edge cases decide how far to push the routine:
- No tests and no time. Write only the characterization tests for the method you must change, not the whole class. The legacy-code workflow is to identify the change points, find the test points, put that code under test, and then change it.
- Dependencies you cannot call. Code that reaches a database or a clock is hard to put under test. Pass the dependency in as an argument so a test can supply a simple stand-in.
- A quirk that is really a bug. Pin it, then tell the interviewer and change it as its own labeled step with its own test, never inside a refactor.
- Cost of insert-anywhere. Inserting into a flat list moves every item after the position: for one thousand items, one thousand moves at the front, five hundred in the middle and none at the end.1 With ten parents holding one hundred children each, a front insert among one parent's children moves one hundred.1
- Tools and teammates propose edits too. The routine does not depend on who writes the change. Feathers says that code with lots of tests is relatively easy to change, so the pinned tests are what tell you whether a proposed edit moved behavior.
For the design questions that follow, see the low level design interview questions.
How to answer it in an interview
You are given this messy class and a new requirement. How do you proceed?
First I read the public methods and write tests that pin what they do today, including the odd edge cases, so I know what must not change. Then I refactor in small steps, running the tests after each one, so a failure points at the last step. Only when the shape is clean do I write a failing test for the new requirement and make the smallest change that passes it. If a quirk looks like a bug, I flag it and fix it as a separate step, never inside the refactor.
The spoken structure: say what you will pin, say you will run the tests after every small step, say the new feature comes last and test-first, and name the separate step for any bug you find.
Common mistakes
- "I'll rewrite it cleanly and add tests afterward." (wrong) Without tests first you cannot tell lost behavior from a new bug. (right) "I pin the current behavior first, then change the structure."
- "I fixed the odd negative-index behavior while tidying." (wrong) A refactor must not change behavior, and the pinned test fails. (right) "That looks like a bug; I'll note it and fix it as its own step with its own test."
- "I refactored and added the feature in one go." (wrong) A failure could come from either change. (right) "I finished the structural change, ran the tests, and then added the feature."
- "Refactoring means cleaning up." (wrong) Fowler calls refactoring one specific technique, not a word for any cleanup. (right) "I make behavior-preserving steps and run the tests after each."
- "I ran the tests once at the end." (wrong) One run at the end cannot say which step broke them. (right) "I run the suite after every small move."
Follow-up questions
How do you add nesting to the to-do list after the flat version works?
Write a failing test for one child under one parent, then add an optional parent path whose default means top level, so the old tests keep passing. Rendering and toggling then walk the tree; the traversal idea is the same one used in nested list weight sum.
How do you start when you are given a large codebase and no tests?
Say that code without tests is what Feathers calls legacy code. Read from the entry point to find the change point, then write characterization tests for the methods you must touch before changing anything. Ask whether the interviewer wants a full suite or only coverage of the change point.
Should you refactor first or add the feature first?
Refactor first when the current structure makes the feature awkward, then add the feature, and keep the two as separate steps so each can be undone alone. If the feature is a one-line change, patching directly and tidying afterward is reasonable; say which you chose and why.
How do you know a refactoring step did not change behavior?
You know it only as far as your tests cover it, which is why the pinning step looks at edges and not just the normal case. A passing suite after each small step is evidence, not proof; say what the tests do not cover.
What if the list grows very large?
A flat list shifts every later item, so an insert is O(n). A linked list removes the shift but still walks to the position, so it helps only when you already hold the neighbour node, for example through a hash map from item id to node, as in the least recently used (LRU) cache implementation in Python. For frequent inserts by index, a balanced tree that stores subtree sizes finds the position and inserts in O(log n). Measure before you change structure, and keep the tests green while you do.
Where does this routine show up in other coding rounds?
Multi-level rounds that grow a system with each new requirement, such as the CodeSignal in-memory database assessment and event and log processing problems, reward the same habit: keep earlier levels passing while you extend.
- Refactoring changes structure without changing behavior, in small steps with a check after each.
- Pin current behavior with characterization tests before the first change.
- Add the new feature after the restructure, test-first, as a separate step.
- A quirk that looks like a bug gets its own labeled step and its own test.
- A passing suite is evidence of preserved behavior only as far as the tests reach.
Sources
- Refactoring (home page and definition) — Martin Fowler, refactoring.com (accessed 2026-10-26), 26 Oct 2026
- Refactoring, chapter Construction — University College London, Research Software Development course (accessed 2026-10-26), 26 Oct 2026
- Working Effectively with Legacy Code (review and summary) — Better Scientific Software (accessed 2026-10-26), 26 Oct 2026
- Michael Feathers interview, Tech Lead Journal episode 195 — Tech Lead Journal, 14 Oct 2024
- Notes from Martin Fowler's Refactoring (reader's page-numbered quotes) — GitHub gist by deanhunt, secondary (accessed 2026-10-26), 26 Oct 2026
- What is test-driven development (TDD)? — Tricentis, secondary, quotes Fowler (accessed 2026-10-26), 26 Oct 2026