One of my recent weekend side projects, an e-ink / raspberrypi driven build status dashboard, was a great playground for doing TDD powered by visual snapshots. But let's rewind a bit.
What I actually wanted to achieve was the following: Build a semi-decent python class to draw a dashboard type interface, which I can feed to my e-ink display. I had already prototyped such a script, but it was a "make it work in the quickest possible way in 1 hour" mess. Nothing I wanted to maintain or even look at for even five more minutes. I also didn't want to start completely from scratch regarding the output, because I was happy enough with the result this script produced, which is shown here:
So how could I develop the code from scratch, while making sure I got the exact same output in the end? Right, creating myself a feedback loop that will quickly compare the reference image to the current output. To quote from jest:
Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly.
A typical snapshot test case for a mobile app renders a UI component, takes a screenshot, then compares it to a reference image stored alongside the test.
This is powerful, because how else would I test this? Things of visual nature are not unit-tested easily, which is why they are often simply untested. We usually don't test stylesheets, colors, images etc. However we can't say those things are unimportant. So I set out to do TDD with snapshots and iterate myself toward the reference result.
Lot of work left to do, sure, but that set myself up for about a two second feedback cycle. The process, which I packaged into a simple
npm test bound to
<leader>t in VIM so I can invoke it in one keystroke, is this:
actual.pngin an "integration" test
See the process encoded here, and yes, the irony of having a node based test invocation for a python script is not lost on me. Computers 🤷🏼♂️
Let's walk through one of my commits together. I really enjoyed working like this. A few minutes in, I had the rendering of the header, header title, project text to the left all fleshed out with minimal differences to the reference. I assume something regarding the font-rendering on the raspberrypi/debian vs. my mac is to be blamed for the tiny deviations around the text. No clue though. So here I was:
Lets add some code to render the badge text on the right:
def __render_row(self, row): self.__render_project_name(row) + self.__render_project_status(row) + + def __render_project_status(self, row): + start_y = (row + 1) * self.__row_height() + badge_width = 150 + padding = 10 + pos = (self.__from_right(badge_width), start_y + padding, self.__from_right(padding), start_y + self.__row_height() - padding) + self.draw.rectangle(pos, outline = self.BLACK) + self.draw.text((self.__from_right(badge_width + 35), start_y + 17), self.PASSED, font = self.badge_font, fill = self.BLACK)
<leader>t, and see this:
So obviously I got the alignment wrong. Lets fix it:
padding = 10 pos = (self.__from_right(badge_width), start_y + padding, self.__from_right(padding), start_y + self.__row_height() - padding) self.draw.rectangle(pos, outline = self.BLACK) - self.draw.text((self.__from_right(badge_width + 35), start_y + 17), self.PASSED, font = self.badge_font, fill = self.BLACK) + self.draw.text((self.__from_right(badge_width - 35), start_y + 17), self.PASSED, font = self.badge_font, fill = self.BLACK) def __render_project_name(self, row): index = row
Re-run the tests, see this:
Less red! That's basically what I did over and over again. Feel free to have a look at the commits for more examples.