Skip to content

Testable code: small checks, fakes and a clock you pass in

How to prove your own code works in five minutes: a tiny check helper, a table of boundary values, a clock and other things you pass in, fakes that stand in for real parts, and how to read a failing check.

  • Lesson 16 of 169
  • 23 min read
  • Core
  • Free to read · No account needed
On this page
  1. Why interviewers ask this
  2. The idea in plain words
  3. Pause and predict
  4. Worked example
  5. Trade-offs
  6. Common misconceptions
  7. How to say it in the interview
  8. Check your understanding
  9. Answers
  10. Summary card: testable code
  11. Sources

Before you start

This lesson uses ideas from these earlier lessons. Read them first if any idea is new to you.

01Why interviewers ask this

In a coding round the hidden tests run on your code, and you cannot see them. Suppose your fee is wrong at exactly 41 minutes. You find out only after a Submit, and the interviewer sees the failure. A bug that you find yourself, before the Submit, costs nothing.

A weak candidate runs one normal example, sees a correct answer and submits. A strong candidate spends five minutes on small checks of their own: a normal case, the edge values, bad input and a repeated call. Their code is also easy to check, because it does not hide the time or the ids inside.

At SDE-1 level you should write a few checks by hand. At SDE-2 level you should also make your code easy to test. Pass in the clock and other outside things, and use a fake in a check. Interviewers often ask: "How would you test this?"

02The idea in plain words

Think of a tailor who has stitched a shirt. A careful tailor does not just look at it. The tailor tries it on and checks the places where it can go wrong: the length of the arms, the neck, the buttons. These are the places where one size changes into the next. Code has the same places. They are called boundaries: the values where a rule changes from one answer to another.

In the Tradeoff round, Run is private. It runs your code and shows the output, and nothing is checked. Submit sends your code to the hidden tests. You can Submit up to 10 times in a round, and the tests do not show you their names or answers. So Run is the place to find your own bugs. This lesson shows how to use Run well.

A tiny check helper

You do not need a test library. A check compares what the code gave with what you expected, and prints one line.

python
def check(name, actual, expected):
    if actual == expected:
        print("ok  ", name)
    else:
        print("FAIL", name, "- got", actual, "expected", expected)

Put your checks in a block that starts with if __name__ == "__main__":. Python sets the name __name__ to "__main__" when you run the file directly. When another file loads yours, the name is the name of the module (the file name). So the block runs when you press Run. When the hidden tests load your file, they skip the block, and your demo does no harm. The Tradeoff test runner is built to ignore such a block.

What to check first

In five minutes you cannot check everything. Check these four things, in this order:

  1. The happy path. One normal call with a clear answer.
  2. Each boundary. Just below the edge, at the edge and just above it.
  3. Bad input leaves the state unchanged. This is the check of the lesson on bad input and errors: make the refused call, then compare.
  4. The same call twice. A second return of the same ticket, or a second cancel of the same order.

A boundary table

For each limit in the rules list, write the three values around it. Put the values in a table, and loop over the table with check. Five rows then cost one short loop. Our example is a cycle rental. The rules are:

  • The first 10 minutes are free.
  • After that, the fee is Rs 5 for each started block of 30 minutes.
  • The fee for one rental is never more than Rs 100.
MinutesWhat it testsFee
9just below the free time0
10at the free time0
11just above: one minute starts a block5
40the block is exactly full5
41one minute into the second block10
580just below the cap95
581the cap is reached100
611past the cap100
The rental fee drawn against minutes, to scale from 0 to 70 minutes.
The fee rises one minute after an edge, not at the edge.
The rental fee drawn against minutes, to scale from 0 to 70 minutes. From 0 to 10 minutes the fee is Rs 0. From 11 to 40 minutes it is Rs 5. From 41 to 70 minutes it is Rs 10. The fee rises one minute after each edge, at minute 11 and at minute 41.

Code that is easy to test

Look at a desk that finds the rental time by calling the real clock inside:

python
import time

class SlowDesk:
    def give_back(self, started_at):
        minutes = int((time.time() - started_at) / 60)
        return rental_fee(minutes)

The function time.time() gives the number of seconds now, so / 60 makes minutes. The function rental_fee is written below. To check the fee for 41 minutes, you must wait 41 minutes. The code hides a thing from outside: the time. The same is true for ids that come from a counter, and for a random choice.

The fix is to pass them in. This is the dependency injection of lesson 13 (the letter D). The class asks for what it needs in its constructor.

  • Time: pass in a clock, an object with one method now(). The real clock reads the computer. The fake clock is a number that you move by hand. Our clock counts whole minutes to keep the code short. A real clock counts seconds, so the real one would divide by 60.
  • Ids: pass in an object with next().
  • A random choice: pass in the function that chooses. Python's random.choice is the normal one. A check passes a function that always takes the first item.
python
import random

def first(items):
    return items[0]

class Fleet:
    def __init__(self, bikes, choose=random.choice):
        self._free = list(bikes)
        self._choose = choose

The part choose=random.choice gives the argument a default value. If the caller passes nothing, Python uses random.choice. A check writes Fleet(["B1", "B2", "B3"], choose=first). Now the first bike is always B1, and a check can name it.

Fakes and stubs

The lesson on interfaces introduced the fake: a small class you write that takes the place of a real one. Three kinds of stand-in are common:

  • A stub gives fixed answers. A clock that always says minute 41 is a stub.
  • A fake works in a simple way. A store that keeps rows in a list instead of a file is a fake. The MemoryOrderStore of lesson 13 is one. Our desk keeps its rentals in a dictionary, so it needs no store.
  • A recording fake remembers what it was asked. Some people say "mock" or "spy". A class that sends messages (a notifier), and keeps every message in a list for a check to read, is a recording fake.

For the cycle rental we need a fake clock that we can move, and a recording notifier.

python
class FakeClock:
    def __init__(self, start=0):
        self._now = start

    def now(self):
        return self._now

    def advance(self, minutes):
        self._now += minutes
python
class RecordingNotifier:
    def __init__(self):
        self.messages = []

    def send(self, text):
        self.messages.append(text)

Reading a failing check

A check fails. Do not read the whole program. Shrink it: find the smallest call that still gives the wrong answer. If a desk gives a wrong fee, call rental_fee alone with the same minutes. If that is also wrong, the bug is in rental_fee. If it is right, the bug is in how the desk finds the minutes. Then fix one thing and run all the checks again, because a fix can break a check that passed.

How many checks, and the standard library

In a five-minute pause, 6 to 8 checks are enough: one normal case, the boundaries that matter, one bad input and one repeated call. A loop over a table counts as one check. Python also has a standard test library called unittest. It needs a class and special method names, and it prints dots and OK. It is fine at work. In a round, the check helper is faster to write and easier to read aloud.

Want to check your understanding? Try explaining this idea in your own words. You can continue reading first, or ask the AI tutor a question. Sign up free to ask the tutor

03Pause and predict

Pause. Guess first.

A rental starts at minute 0 and ends at minute 41. The first 10 minutes are free, then Rs 5 for each started 30 minutes. What is the fee? And how would you check this in a round without waiting 41 minutes?

Write your guess before you read on. The answer is in the worked example.

04Worked example

Run with Python 3.11. Assumption: the rules are the ones above. The rule "Rs 5 for each started 30 minutes" is the tricky part. After the free time, 1 minute starts a block, and so does 30 minutes. So we round up. In whole numbers, rounding up a division by 30 is (extra + 29) // 30, where // divides and drops the fraction.

First, the fee function, with the constants at the top (one place for each number, as in lesson 14).

python
FREE_MINUTES = 10
BLOCK_MINUTES = 30
RATE = 5
RENTAL_CAP = 100

def rental_fee(minutes):
    if minutes < 0:
        raise ValueError("minutes cannot be negative")
    extra = minutes - FREE_MINUTES
    if extra <= 0:
        return 0
    blocks = (extra + BLOCK_MINUTES - 1) // BLOCK_MINUTES
    return min(blocks * RATE, RENTAL_CAP)

The call min(a, b) gives the smaller of the two numbers.

The predict box. For 41 minutes, the extra time is 41 - 10 = 31 minutes. Rounding up 31 / 30 gives 2 blocks, so the fee is 2 x 5 = Rs 10. To check it without waiting, we pass in the fake clock. The desk takes a clock, an id counter and a notifier in its constructor.

python
class Ids:
    def __init__(self):
        self._last = 0

    def next(self):
        self._last += 1
        return self._last

class RentalDesk:
    def __init__(self, clock, ids, notifier):
        self._clock, self._ids, self._notifier = clock, ids, notifier
        self._open = {}

    def rent(self):
        ticket = self._ids.next()
        self._open[ticket] = self._clock.now()
        return ticket
python
    def give_back(self, ticket):
        if ticket not in self._open:
            raise ValueError(f"unknown ticket: {ticket}")
        minutes = self._clock.now() - self._open[ticket]
        fee = rental_fee(minutes)
        del self._open[ticket]
        self._notifier.send(f"Ticket {ticket}: Rs {fee}")
        return fee

The method give_back follows the lesson on bad input and errors: it checks the ticket first, then changes the data. The line del self._open[ticket] removes the key from the dictionary. Now the demo block, with the boundary table and a desk check.

python
if __name__ == "__main__":
    table = [(9, 0), (10, 0), (11, 5), (40, 5), (41, 10),
             (580, 95), (581, 100), (611, 100)]
    for minutes, expected in table:
        check(f"{minutes} minutes", rental_fee(minutes), expected)

    clock, notes = FakeClock(), RecordingNotifier()
    desk = RentalDesk(clock, Ids(), notes)
    ticket = desk.rent()
    clock.advance(41)
    check("fee after 41 minutes", desk.give_back(ticket), 10)
    check("message", notes.messages, ["Ticket 1: Rs 10"])

The line for minutes, expected in table takes each pair of the table and names its two parts. The demo prints ok for all eight rows of the table, then ok fee after 41 minutes and ok message. The fee is 10, as we worked out by hand. The check took no waiting, because the test moved the clock by 41 with clock.advance(41).

Two more checks cover the third and fourth items of the list. The second return of the same ticket is bad input, because the ticket is gone, and it is also the same call twice. It must be refused, and it must send no second message.

python
    try:
        desk.give_back(ticket)
        check("second return refused", "no error", "error")
    except ValueError:
        check("second return refused", "error", "error")
    check("only one message", len(notes.messages), 1)

Both print ok. That is one table loop with 8 rows, and 4 desk checks. They take well under five minutes to write.

A failing check. Suppose the first draft of rental_fee used blocks = extra // BLOCK_MINUTES. The table then prints:

text
ok   9 minutes
ok   10 minutes
FAIL 11 minutes - got 0 expected 5
ok   40 minutes
FAIL 41 minutes - got 5 expected 10
ok   580 minutes
FAIL 581 minutes - got 95 expected 100
ok   611 minutes

Three rows fail. We shrink to the smallest call: 11 minutes. The extra time is 1 minute, and 1 // 30 is 0 blocks, so the fee is 0. But a started block must count, so the blocks must round up. The fix is the line with (extra + BLOCK_MINUTES - 1). Notice that the row for 611 minutes passed even with the bug, because the cap hid the error. Only the boundary rows 41 and 581 showed it. This is why we check just below and just above each edge.

05Trade-offs

Small checks and passed-in parts solve the wait. You find the bug in the minutes before the Submit, and a fake clock lets you test 41 minutes without waiting. They cost time in a short round, and a few more lines: a constructor that takes the parts, and the fake classes. I would write fewer checks, and skip the fakes, when the problem has no time, no ids and no random choice. Then a normal case and the boundaries are enough. I would write more checks when the interviewer says the rules are tricky.

06Common misconceptions

"My example gave the right answer, so the code is right." One example tests one path. In the example above, the row for 611 minutes passes even with a wrong formula, because the cap hides the error. The bugs sit at the edges, so check just below and just above each limit.

"A test needs a test library." A function that compares two values and prints one line is enough for a round. A library helps in a big project, but it costs setup time you do not have.

"A fake clock is cheating, because the real clock is different." The rental rule does not care where the minute came from. The real clock is a tiny class with one method. The fake lets you test the rule that matters.

07How to say it in the interview

Say it like this

Before I submit, I check the normal case, the values just below, at and above each limit, bad input and a repeated call. I pass in the clock and the ids, so I can test a 41-minute rental without waiting.

08Check your understanding

  1. What is a boundary? Name the three values you check for each limit.
  2. A rental is returned at minute 70, and the rule is the one above. What is the fee? Then list the fee for 69, 70 and 71 minutes, and say which rows are boundaries of the block.
  3. Your check with FakeClock for a desk fee fails: it expects Rs 10 and gets Rs 5. What are your first two steps?

09Answers

Show the answers
  1. A boundary is a value where the rule changes from one answer to another. For each limit, check just below, at, and just above it.
  2. 70 - 10 = 60 extra minutes. 60 / 30 = 2 blocks exactly, so the fee is 2 x 5 = Rs 10. For 69: extra 59, round up to 2 blocks, Rs 10. For 70: extra 60, 2 blocks, Rs 10. For 71: extra 61, round up to 3 blocks, Rs 15. The rows 70 and 71 are the boundary of the second block. The value 70 fills it exactly, and 71 starts the third.
  3. First, shrink: call rental_fee alone with the minutes that the desk found. If rental_fee is right, the bug is in how the minutes are found. Print the minutes that the desk worked out. For example, the clock may have moved by the wrong amount. Second, fix one thing and rerun every check.

10Summary card: testable code

One-page summary

  • Idea. Run is free and private. Find your own bugs before a Submit.
  • Helper. check(name, actual, expected) prints one line. Put the demo under if __name__ == "__main__":.
  • Four first checks. A normal case, each boundary, bad input changes nothing, the same call twice.
  • Boundary table. Just below, at, just above each limit. Example: 40 gives 5, and 41 gives 10.
  • Easy to test. Pass in the clock, the ids and the random choice. A stub gives fixed answers, and a recording fake remembers calls.
  • Failing check. Shrink to the smallest call, fix one thing, rerun all.
  • Numbers. About 6 to 8 checks in five minutes.
  • Trade-off. It solves late bugs and long waits. It costs minutes and a constructor.
  • Trap. One normal example, and a real clock inside the class.

11Sources

  • Tradeoff machine-coding practice round (the round rules page): Run is private and nothing is checked. Submit runs the hidden tests, with up to 10 Submits in a round. A demo under __main__ is ignored by the Python test runner. Other companies use other rules.
  • __name__ and "__main__": Python documentation, https://docs.python.org/3/library/__main__.html (opens in a new tab) (read 29 Oct 2026).
  • The cycle rental, its fees and the cap are made up for teaching. All code was run with Python 3.11.

Try what you just learned

Put this idea into practice with a short exercise or a relevant tool. Free to try. No sign-up needed.

More practice. This needs a free account.

Continue learning with the lessons that build on this one.

Practise this idea

Have a question about this lesson?

Ask the AI tutor to explain a difficult idea, walk through an example or check your understanding. It uses the lesson to help you work through the topic.

A free account includes 10 AI tutor questions a day. The lesson remains free to read without an account.

All lessons in Module 2: Low-level design and machine coding

Prefer another topic? Browse all 169 lessons.