Lazydoro Mk 3 - lots of automated tests for a simple design
I had to rely on a web-based Pomorodo timer, and thought I'd have another try at the lazydoro project.
I decided to rewrite lazydoro from scratch.
Lazydoro needs to do four things.
- It needs to know when I arrive at or leave my desk
- It needs to keep track of passing time
- It needs to know where I am in a Pomodoro cycle
- It needs to provide feedback to keep me on track.
To make lazydoro easy to test I used a variant of the Ports and Adapters architecture. (Ports and Adapters is sometimes called Hexagonal Architecture).
I first came across it in an article by Alistair Cockburn, and it made a lot of sense. It's also featured in the GOOS book: Growing Object Oriented Software Guided by Tests.
Here's the architecture for lazydoro:
At the centre is the application model: the Pomodoro object. That contains code that keeps track of where I am in the Pomodoro Cycle. It's a state machine, and it changes state based on my presence or absence and the passage of time.
The Pomodoro model gets inputs from a ClockWatcher.
The ClockWatcher gets a tick that tells it time has passed. It then asks the rangefinder how close the nearest object is, and from that it works out whether I've just arrived at or left my desk and sends a message to the Pomodoro object as necessary .
As the Pomodoro tracks the various states (waiting to start, in a Pomorodo, waiting for me to begin a break, on a break, or waiting for me to return). it updates the Display, turning LEDs on or off and sounding the buzzer as appropriate.
The great advantage of that approach is that I can easily test the application. I can send a message pretending that a second has passed as often as I like. That's really helpful because it allows me to run a test much faster than real time. A real Pomodoro cycle takes 30 minutes: 25 minutes of work and a 5-minute break. I can simulate a full cycle in under a second if my fake clock ticks fast enough.
The automated tests use Mock Objects to represent the RangeFinder and Display.
As a result, the tests are simple and expressive. Here's the test code for a full Pomodoro cycle:
def test_tracks_full_pomodoro(self): # main success scenario self.person_absent() self.wait(seconds=1) self.check_leds_are_off() self.person_present() self.check_leds_are_off() self.wait(seconds=10) assert_that(self.display, shows_only(BLUE)) self.wait(minutes=24) assert_that(self.display, shows_all(BLUE)) self.wait(minutes=5) assert_that(self.display, shows_all(RED)) self.person_absent() self.wait(seconds=20) assert_that(self.display, shows_only(GREEN)) self.wait(minutes=1) assert_that(self.display, shows_only(GREEN, GREEN)) self.wait(minutes=1) assert_that(self.display, shows_only(GREEN, GREEN, GREEN)) self.wait(minutes=1) assert_that(self.display, shows_only(GREEN, GREEN, GREEN, GREEN)) self.wait(minutes=1) assert_that(self.display, shows_only(GREEN, GREEN, GREEN, GREEN, GREEN)) self.wait(minutes=1) self.check_leds_are_off() self.person_present() self.wait(seconds=20) assert_that(self.display, shows_only(BLUE))
Success at last?
It worked well for weeks, and I used lazydoro every day.
And then it stopped working.
What went wrong?
The code was fine. The problem was physical.
I got a new chair for my study.
It had much better support for my back, but lazydoro's ToF sensor sometimes thought the chair was me! It picked up a reflection from the back of the chair, and it sometimes thought I was at my desk doing a Pomodoro even when the chair was empty.
A new beginning
I needed a new approach.
I decided to ask for help in one of the Facebook groups I belong to, and got lots of interesting suggestions.
Tomorrow I'll reveal what happened next.