Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

MicroPython for Microcontrollers  Module 12 – Testing with the unix port

Fakes for machine, time and network

Test MicroPython board code on a computer with fakes for machine, time and network: hardware passed in, a clock that wraps, buses and Wi-Fi that fail on cue.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: Pass the hardware in, and a fake can take its place · A fake clock brings the tick wrap into the test · Check yourself. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Replace hardware modules with fakes in host tests by passing objects in rather than shadowing modules
  • Build a fake clock whose ticks wrap at 2**30, and use it to catch a wrap bug in milliseconds
  • Script bus faults such as OSError with EIO and ENODEV in a fake I2C, and test a driver's retries
  • Simulate Wi-Fi joins with a fake WLAN that returns each port's documented status codes
  • Check a fake against the real module with one contract test that both must pass

Before you start

What you’ll learn · Before you start

What you will learn

  • Replace hardware modules with fakes in host tests by passing objects in rather than shadowing modules
  • Build a fake clock whose ticks wrap at 2**30, and use it to catch a wrap bug in milliseconds
  • Script bus faults such as OSError with EIO and ENODEV in a fake I2C, and test a driver's retries
  • Simulate Wi-Fi joins with a fake WLAN that returns each port's documented status codes
  • Check a fake against the real module with one contract test that both must pass

Before you start

On this page

A greenhouse controller switches a pump with machine.Pin, reads the soil through machine.ADC and times the pump with time.ticks_ms(), and it has a bug that shows only when the tick counter wraps, once every 12.4 days. Nobody waits 12 days beside a board for a test; with a fake clock: An object with ticks_ms(), ticks_add() and ticks_diff() that a test moves forward by hand. It keeps ticks modulo 2**30 like a 32-bit board, so it can start just before the wrap., the wrap arrives in the first millisecond.

This lesson builds fakes for machine, time and network, small objects with the same methods as the real ones, and the rules that keep them honest. The programs run on CPython and are written in the part of Python that MicroPython shares, as host tests are.

Pass the hardware in, and a fake can take its place

There are two ways to put a fake: A small stand-in for a module or an object, used in tests: a fake machine module whose I2C class returns prepared bytes lets a driver run without the hardware. More in “Imports, modules and packages on a device” under board code. One is to shadow the module: a file named machine.py earlier on the path replaces the built-in machine on the unix port and on a board, because machine is an extensible module, and CPython has no machine of its own. Shadowing is fragile, though. A file named time.py wins on MicroPython and loses on CPython, whose time is built into the interpreter, and a fake file that reaches a board replaces the real module there. The other way is to pass the hardware in, the dependency injection: Giving an object the things it works with, such as a bus, as arguments instead of letting it create them. A driver that takes its bus this way runs on any bus, a fake one in a test included. More in “Write a reusable device driver class” of the drivers lesson: the controller receives its pin, its ADC and its clock as arguments and creates none of them, so on the board main.py passes Pin(15, Pin.OUT), ADC(26) and the time module itself, and a test passes fakes. Follow the controller through both:

Interactive · step by step One controller, given the board's objects or fakes Step through the controller's calls, first on a board and then in a test. Only what is passed in changes.
One controller, given the board's objects or fakes, step 5 of 5. Showing Greenhouse, main.py (the test), pump (FakePin), soil (FakeADC([35000])), clock (FakeClock(2**30 - 1000)). In focus: pump.passes them invalue(1)read_u16()ticks_ms()Greenhousecreates no hardwaremain.pythe testpumpFakePinsoilFakeADC([35000])clockFakeClock(2**30 - 1000)value(0)

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. On the board. main.py creates the pump's pin, the sensor's ADC, and passes them to Greenhouse with the time module itself as the clock, since time has ticks_ms() and ticks_diff().
  2. The calls it makes. Each step reads the soil with read_u16(), reads the time with ticks_ms(), and switches the pump with value(). Those three calls are everything the controller needs from the hardware.
  3. In a test. A test passes FakePin, FakeADC and FakeClock instead. They have the same methods, so the controller runs unchanged, on a computer, with no board attached.
  4. Time on demand. The fake clock starts one second before the tick counter wraps, and advance() moves it on at once, so 32 seconds of pumping take no time and the wrap arrives on the first step.
  5. What the test reads. The test reads the fake pin's history, 0, then 1, then 0, and fails when the pump never went off, which is how the wrap bug of the next section shows itself.

Fakes for machine are the first kind a board’s tests need: a pin that remembers its levels, an ADC that returns prepared readings, a bus that answers from a table. Each copies only the real module’s methods that the code needs and adds none of its own, so a call that the board’s module does not offer fails in the test as well. Time and network follow the same rule. A fake network module and WLAN return the documented status codes in a scripted order, a fake I2C bus raises the board’s errors on chosen calls, and one contract test: A test of what code relies on from a module, such as a bus that returns bytes of the length asked for, run against the fake on a computer and against the real module on a board., run on the fake and on the real module, keeps each fake true to the board.

Checkpoint

Check: Passing hardware in instead of shadowing modules

  1. Question 1 of 2 A test folder holds a fake time.py and is first on the path. Which time module do the tests import on CPython and on the unix port?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: CPython's own time on CPython, because it is built into the interpreter; the fake on the unix port, where time is extensible

    Passing a clock object in avoids the question altogether: the code uses whatever it is given.

  2. Question 2 of 2 The greenhouse controller takes its clock as an argument. What does main.py pass on a board?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The time module itself, which has ticks_ms() and ticks_diff()

    On the board the real objects, in a test the fakes: the controller's code is the same.

A fake clock brings the tick wrap into the test

On the rp2 and esp32 ports the tick counters wrap at 2³⁰, and time.ticks_diff() gives the right difference across the wrap while the two values are less than half that period apart. The unix port of a 64-bit computer wraps at 2⁶², so a test there never meets the wrap a Pico meets every 12.4 days. A fake clock fixes both: it keeps ticks modulo 2³⁰, has ticks_ms(), ticks_add() and ticks_diff(), and advance() moves it on at once. Step through the controller’s bug, the elapsed time computed as plain subtraction, with the start one second before the wrap:

Interactive · line by line Elapsed time across the tick wrap, two ways Watch wrong and right on each pass. The counter passed 2**30 and started again from 0 between the start and now.

Program · fakes/wrap_bug.py

PERIOD = 1 << 30
HALF = PERIOD // 2


def diff(a, b):
    return (a - b + HALF) % PERIOD - HALF


started = PERIOD - 1000
for after in (10_000, 31_000):
    now = (started + after) % PERIOD
    wrong = now - started
    right = diff(now, started)
    print(after, wrong, right)

Variables

After line 10: PERIOD is 1073741824; HALF is 536870912; diff is diff(a, b); started is 1073740824; after is 31000; now is 30000; wrong is -1073710824; right is 31000; the loop's own list from (10_000, 31_000) is (10000, 31000) (the loop stopped at position 2).PERIOD1073741824HALF536870912diffdiff(a, b)started1073740824after31000now30000wrong-1073710824right31000the loop reads (10_000, 31_000)1000003100012end
  • just taken by the loop
  • changed in this step
  • passed by the loop
  • read next
  • where the loop stopped

Printed so far

10000 -1073731824 10000
31000 -1073710824 31000

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 16 steps are listed below.

Line run 10
Next line end
Loop pass 2
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. PERIOD = 1 << 30 sets PERIOD to 1073741824.
  3. Line 2. HALF = PERIOD // 2 sets HALF to 536870912.
  4. Line 5. def diff(a, b): makes the function diff. Its body runs only when it is called.
  5. Line 9. started = PERIOD - 1000 sets started to 1073740824. started is 1,073,740,824: one second before the counter wraps to 0, as a board reaches every 12.4 days.
  6. Line 10. for after in (10_000, 31_000): takes the item at position 0 of (10_000, 31_000): after is 10000.
  7. Line 11. now = (started + after) % PERIOD sets now to 9000.
  8. Line 12. wrong = now - started sets wrong to -1073731824. The bug Ten seconds later now is 9,000, after the wrap, so now - started is a huge negative number. A check of wrong >= 30_000 is never true, and the pump would never stop.
  9. Line 13. right = diff(now, started) sets right to 10000. The fix ticks_diff() shifts by half the period, takes the remainder and shifts back, so it gives 10,000, the time that passed.
  10. Line 14. print(after, wrong, right) prints 10000 -1073731824 10000.
  11. Line 10. for after in (10_000, 31_000): takes the item at position 1 of (10_000, 31_000): after is 31000.
  12. Line 11. now = (started + after) % PERIOD sets now to 30000 (it was 9000).
  13. Line 12. wrong = now - started sets wrong to -1073710824 (it was -1073731824).
  14. Line 13. right = diff(now, started) sets right to 31000 (it was 10000).
  15. Line 14. print(after, wrong, right) prints 31000 -1073710824 31000.
  16. Line 10. for after in (10_000, 31_000): finds no item at position 2 of (10_000, 31_000) (it has 2 items), so the loop ends. That was the last line, so the program ends. A test on the unix port of a 64-bit computer never sees this, because its ticks wrap at 2**62. A fake clock that starts just before the wrap shows it in milliseconds.

The program below runs three tests on two versions of the controller, the right one and the one that subtracts, with the fake clock started at 0 and at one second before the wrap:

Three tests of the greenhouse controller, with the clock at 0 and just before the wrap

fakes/test_greenhouse.py

"""Three tests of the greenhouse controller, run on both versions, with the fake clock started at 0 and at one second
before the tick counter wraps. A tiny runner prints each result."""
from fakes import PERIOD, FakeADC, FakeClock, FakePin
from greenhouse import DRY, Greenhouse, GreenhouseWithBug


def run_for(controller, clock, ms, every=1000):
    for _ in range(ms // every):
        controller.step()
        clock.advance(every)


def test_dry_soil_starts_pump(cls, start):
    pump, clock = FakePin(), FakeClock(start)
    cls(pump, FakeADC([DRY + 5000]), clock).step()
    assert pump.value() == 1


def test_wet_soil_stops_pump(cls, start):
    pump, clock = FakePin(), FakeClock(start)
    g = cls(pump, FakeADC([DRY + 5000, DRY + 5000, DRY - 4000]), clock)
    run_for(g, clock, 3000)
    assert pump.history == [0, 1, 0], pump.history


def test_pump_stops_after_30_s(cls, start):
    pump, clock = FakePin(), FakeClock(start)
    run_for(cls(pump, FakeADC([DRY + 5000]), clock), clock, 32_000)
    assert pump.history[:3] == [0, 1, 0], "the pump still ran after 32 s"


for cls in (Greenhouse, GreenhouseWithBug):
    for start in (0, PERIOD - 1000):
        results = []
        for test in (test_dry_soil_starts_pump, test_wet_soil_stops_pump, test_pump_stops_after_30_s):
            try:
                test(cls, start)
                results.append("ok")
            except AssertionError:
                results.append("FAIL " + test.__name__[5:])
        print("%-17s clock from %10d: %s" % (cls.__name__, start, ", ".join(results)))

Output

Greenhouse        clock from          0: ok, ok, ok
Greenhouse        clock from 1073740824: ok, ok, ok
GreenhouseWithBug clock from          0: ok, ok, ok
GreenhouseWithBug clock from 1073740824: ok, ok, FAIL pump_stops_after_30_s

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 test_greenhouse.py

fakes/greenhouse.py

"""A greenhouse pump controller, as it runs on a board. It creates no hardware itself: the pump pin, the soil sensor's
ADC and the clock are passed in. On a board the clock is the time module itself, which has ticks_ms() and
ticks_diff(); in a test it is a fake."""
DRY = 30_000  # read_u16() above this: the soil is dry
MAX_PUMP_MS = 30_000  # never run the pump longer than this in one go


class Greenhouse:
    def __init__(self, pump, soil, clock):
        self.pump, self.soil, self.clock = pump, soil, clock
        self.started = None

    def step(self):
        now = self.clock.ticks_ms()
        if self.started is None:
            if self.soil.read_u16() > DRY:
                self.pump.value(1)
                self.started = now
        elif self.clock.ticks_diff(now, self.started) >= MAX_PUMP_MS or self.soil.read_u16() <= DRY:
            self.pump.value(0)
            self.started = None


class GreenhouseWithBug(Greenhouse):
    """The same controller with the comparison a board meets every 12.4 days: plain subtraction of ticks."""

    def step(self):
        now = self.clock.ticks_ms()
        if self.started is None:
            if self.soil.read_u16() > DRY:
                self.pump.value(1)
                self.started = now
        elif now - self.started >= MAX_PUMP_MS or self.soil.read_u16() <= DRY:
            self.pump.value(0)
            self.started = None

fakes/fakes.py

"""Fakes for the three things the greenhouse controller uses: a pin, an ADC and a clock. They copy only the board's
methods that the controller calls and add none of their own, so a call the board does not have fails in the test too.

FakeClock keeps ticks the way MicroPython's 32-bit ports do (extmod/modtime.c and py/mpconfig.h, v1.29.0): they wrap
at 2**30, ticks_add() and ticks_diff() work modulo that period, and ticks_diff() is right while the two values are less
than half a period apart."""
PERIOD = 1 << 30
HALF = PERIOD // 2


class FakePin:
    def __init__(self, value=0):
        self._value = value
        self.history = [value]

    def value(self, v=None):
        if v is None:
            return self._value
        self._value = 1 if v else 0
        self.history.append(self._value)


class FakeADC:
    """read_u16() returns the readings it was given, one per call, then the last one again."""

    def __init__(self, readings):
        self._readings = list(readings)

    def read_u16(self):
        return self._readings.pop(0) if len(self._readings) > 1 else self._readings[0]


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

    def ticks_ms(self):
        return self._now

    def ticks_add(self, ticks, delta):
        return (ticks + delta) % PERIOD

    def ticks_diff(self, ticks1, ticks2):
        return ((ticks1 - ticks2 + HALF) % PERIOD) - HALF

    def advance(self, ms):
        self._now = (self._now + ms) % PERIOD

From 0 both versions passed everything, which is what a test with the real clock of a computer sees. Started just before the wrap, the subtracting version kept the pump running past 30 s, and only that test failed: the bug a board meets after 12 days, found in milliseconds.

Key idea

Give board code its time as an object, and let tests start that clock just before 2³⁰: a fake clock turns a bug that takes 12.4 days to appear into one that a test finds at once.

Checkpoint

Check: A fake clock that wraps

  1. Question 1 of 2 Why does a test of the subtracting controller pass on the unix port of a 64-bit computer, even after a long run?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Its ticks wrap at 262 instead of 230, so the computer never meets the wrap that a Pico meets every 12.4 days

    Only a fake clock that wraps at 2**30 brings the board's behaviour into a test on the computer.

  2. Question 2 of 2 Where should a test start the fake clock to find wrap bugs in a 30-second pump timeout?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A little before 2**30, such as one second before, so the counter wraps during the pump's 30 seconds

    Put the edge case where the code measures: just before the counter wraps.

Check yourself

Write the three fakes the greenhouse tests used, then work through the self-check on passing hardware in and on fake clocks.

Exercise · Medium · Python

Fakes for a pin, an ADC and a clock that wraps

Write three fakes in myfakes.py for host tests of board code. Each has only the methods listed.

Pin(value=0): value() returns the pin's level as 0 or 1; value(v) sets it to 1 when v is true and to 0 otherwise. history is a list of every level the pin has had, starting with the first.

ADC(readings): read_u16() returns the readings in order, one per call, and then the last one at every further call. Every reading must be an integer from 0 to 65535: the constructor raises ValueError otherwise, and for an empty list.

Clock(start=0): ticks_ms() returns the current value, which starts at start modulo 2**30; advance(ms) moves it on, wrapping at 2**30. ticks_add(t, delta) returns t + delta modulo 2**30, and raises OverflowError when delta is not strictly between -2**29 and 2**29. ticks_diff(a, b) returns a - b as a signed number between -2**29 and 2**29 - 1, so it is right while the two values are less than half the period apart.

Starter code · myfakes.py

PERIOD = 1 << 30
HALF = PERIOD // 2


class Pin:
    def __init__(self, value=0):
        self.history = [value]

    def value(self, v=None):
        return 0


class ADC:
    def __init__(self, readings):
        self.readings = list(readings)

    def read_u16(self):
        return 0


class Clock:
    def __init__(self, start=0):
        self.now = start

    def ticks_ms(self):
        return self.now

    def advance(self, ms):
        self.now += ms

    def ticks_add(self, t, delta):
        return t + delta

    def ticks_diff(self, a, b):
        return a - b
The sample tests · test_myfakes.py
from myfakes import ADC, HALF, PERIOD, Clock, Pin


def test_pin_levels_and_history():
    """value(v) stores 0 or 1, value() reads it, history keeps every level"""
    pin = Pin()
    pin.value(True)
    pin.value(0)
    pin.value(7)
    assert pin.value() == 1
    assert pin.history == [0, 1, 0, 1]


def test_adc_repeats_the_last_reading():
    """readings come in order, then the last one again"""
    adc = ADC([100, 40000])
    assert [adc.read_u16() for _ in range(4)] == [100, 40000, 40000, 40000]


def test_adc_refuses_impossible_readings():
    """65536, -1 and an empty list are not readings an ADC can give"""
    for bad in ([65536], [-1], []):
        try:
            ADC(bad)
        except ValueError:
            continue
        raise AssertionError("ADC(%r) did not raise ValueError" % (bad,))


def test_clock_wraps():
    """advance() wraps at 2**30, like a board's ticks_ms()"""
    clock = Clock(PERIOD - 1000)
    clock.advance(1500)
    assert clock.ticks_ms() == 500


def test_ticks_diff_across_the_wrap():
    """500 ms after a reading taken 1,000 ms before the wrap is 1,500 ms later"""
    clock = Clock()
    assert clock.ticks_diff(500, PERIOD - 1000) == 1500
    assert clock.ticks_diff(PERIOD - 1000, 500) == -1500


def test_ticks_diff_range():
    """half a period apart, ticks_diff() turns negative"""
    clock = Clock()
    assert clock.ticks_diff(HALF - 1, 0) == HALF - 1
    assert clock.ticks_diff(HALF, 0) == -HALF


def test_ticks_add():
    """ticks_add() wraps, and refuses deltas of half a period or more"""
    clock = Clock()
    assert clock.ticks_add(PERIOD - 10, 30) == 20
    assert clock.ticks_add(5, -10) == PERIOD - 5
    for bad in (HALF, -HALF):
        try:
            clock.ticks_add(0, bad)
        except OverflowError:
            continue
        raise AssertionError("ticks_add(0, %d) did not raise OverflowError" % bad)
A hint

For ticks_diff, shift by half the period before taking the remainder and back after it: ((a - b + HALF) % PERIOD) - HALF, with PERIOD = 1 << 30 and HALF = PERIOD // 2.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Practice that adapts to you

Practise this lesson

6 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 6 Why does a fake copy only the real module's methods that the code needs, and add none of its own?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: So that a call the board's module does not have, or the code should not make, fails in the test too

    A fake is a promise about the interface; keep it as narrow as the real one is for this code.

  2. Question 2 of 6 The controller calls clock.ticks_ms() and clock.ticks_diff(). Why can main.py pass the time module itself as clock?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: A module with those two functions is used the same way as an object with those methods

    Passing time on the board and FakeClock in tests is the whole trick.

  3. Question 3 of 6 A tests/fakes/machine.py is copied to a board's /lib by a careless sync. What happens there?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: It replaces the real machine module for every import, because machine is extensible, and the hardware calls stop working

    Keep fakes in a folder that never reaches a board, and prefer passing objects in, which needs no shadowing at all.

  4. Question 4 of 6 At what value does a Pico's time.ticks_ms() wrap back to 0?

    Type a number.

    Show the answer: Question 4 of 6

    Answer: 1073741824 ms

    2**30 = 1,073,741,824 ms, about 12.4 days; the esp32 port uses the same period.

  5. Question 5 of 6 A reading was taken at 230 − 1000 and the next at 500, after the wrap. What does ticks_diff(500, 230 − 1000) return?

    Type a number.

    Show the answer: Question 5 of 6

    Answer: 1500 ms

    1,000 ms to the wrap and 500 after it: 1,500 ms, which ticks_diff() gives because the two values are less than half a period apart. Plain subtraction gives −1,073,740,324.

  6. Question 6 of 6 The pump timeout is written as now - started >= 30_000. On a board, what does a user see after about 12 days?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: Once, the pump runs far past its 30 seconds, because a start just before the wrap makes the subtraction a large negative number

    A bug that happens once every 12.4 days, at random times of day, is exactly the kind a fake clock catches in tests.

Fake buses that fail on cue, and a driver's retries proved Pro: opens with a Pro, Premium or Ultimate pass

A loose cable makes an I2C transfer fail now and then, and a missing sensor makes every one fail, each with its own error. This part builds a bus that raises those errors on chosen calls and puts a driver’s error handling through seven scripted faults.

In this part:

  • Errors scripted per call, and every call recorded
  • Seven drills, and the one that the hardware bus changes

1 runnable example (bus_drills.py, fake_i2c.py, humidity.py) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Fake Wi-Fi that follows each port's status codes Pro: opens with a Pro, Premium or Ultimate pass

A join that works at home can wait for ever at a site whose router is slow, and the two ports report the same states with different numbers. This part builds a fake WLAN from the documented codes and runs two joins against six networks on both ports.

In this part:

  • A script of states, each port’s own numbers
  • Six networks, two joins, and what each number costs

1 runnable example (fake_wlan.py, join.py, wifi_drills.py) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Keeping fakes honest: one contract for the fake and the real module Pro: opens with a Pro, Premium or Ultimate pass

A fake that drifts from the real module lets every test pass and the first board fail. This part writes down what the drivers rely on as a contract, runs it on the fake bus and on four fakes that drifted, and runs the same file on a board.

In this part:

  • The contract: what the drivers rely on, written as checks
  • The same file on the board, and when to run it
  • An exercise: extend the contract to registers and to both missing-device errors

1 runnable example (contract.py, contract_drills.py, fake_i2c.py) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

Warm-up (fresher): Why pass hardware objects into a class instead of creating them inside it?

Because then the class runs anywhere it is given objects with the right methods. A driver that creates machine.I2C(0) itself can only run where that bus exists, on one kind of board, and a test can replace the bus only by shadowing the machine module, which behaves differently on CPython, the unix port and a board. A driver that receives its bus runs on the board’s hardware bus, on a SoftI2C, and in a test on a fake that returns prepared bytes or raises errors on cue. The same holds for clocks and networks: pass in the time module on the board and a fake clock in a test.

Three longer questions on fakes start from this lesson:

  1. Fresher to mid

    An irrigation controller passes all its tests on the unix port in CI, but in the field a Pico now and then runs its pump for hours. The timeout is written as now - started >= 30_000 with values from time.ticks_ms(). Explain the fault and why the tests missed it, and write the test that catches it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    An I2C humidity driver stops the whole app when the sensor's cable is loose, and in the field nobody can tell a loose cable from a missing sensor. Design a fake bus and the tests that make the driver's error handling provably right on a Pico's hardware bus and on SoftI2C.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    Your team's fake machine module passed 300 tests, yet the first real board failed within a minute, because the fake did something the real module never does. How do you keep fakes honest from now on?

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Why is replacing a module with a fake file of the same name fragile?

    A time.py wins on MicroPython, where time is extensible, but loses on CPython, whose time is built in; and a fake file that reaches a board replaces the real module there.

  2. How does board code let a test hand it fakes?

    It receives its pin, its ADC and its clock as arguments and creates none of them: main.py passes Pin(15, Pin.OUT), ADC(26) and the time module, and a test passes fakes.

  3. Which methods should a fake have?

    Just the methods of the real module that the code under test calls, with nothing added, so a call the board would refuse fails in the test too.

  4. Why does a tick-wrap bug never appear when tests run on a 64-bit computer's unix port?

    Its ticks wrap at 2⁶², while rp2 and esp32 wrap at 2³⁰, about every 12.4 days.

  5. What is a fake clock, and how does it catch wrap bugs?

    An object with ticks_ms(), ticks_add() and ticks_diff() that keeps ticks modulo 2³⁰, with advance() to move it on at once. Start it one second before the wrap.

  6. What keeps a fake true to the real module?

    One contract test that both must pass: the checks of what the code relies on from the module, run against the fake on a computer and against the real module on a board.

Key takeaways

  • Passing pins, buses and clocks into code lets a test give it fakes, while shadowing a module behaves differently on CPython, the unix port and a board.
  • A fake copies only the real module’s methods that the code needs and adds none, so a call the board lacks fails in the test too.
  • A fake clock that wraps at 2³⁰ and starts just before the wrap finds a tick bug at once, which neither the unix port of a 64-bit computer nor days of waiting find.

Terms in this lesson

contract test
A test of what code relies on from a module, such as a bus that returns bytes of the length asked for, run against the fake on a computer and against the real module on a board.
fake clock
An object with ticks_ms(), ticks_add() and ticks_diff() that a test moves forward by hand. It keeps ticks modulo 2**30 like a 32-bit board, so it can start just before the wrap.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.