“How do you implement some functionality in a test-driven way, when you don’t know at all how the solution might look like?”, I sometimes get asked.

You know, sometimes I don’t. I just hack away. But bear with me: Sometimes I don’t—until I do. At the end of this post, you’ll see tests and even test-driven code.

I am currently in the middle (by the time I publish this blog, it will be finished) of implementing such a “hack-then-do-right” feature, so I decided to blog about it:

Drawing on Slides

When I present, I love drawing on slides. So, marmota.app—the presentation app I am developing—must support that too. Here’s how it will look like:

Animated GIF: Writing text on a slide during a presentation

What you see here is a very first proof of concept that I quickly hacked together. There are no automated tests, and it does not even work correclty yet: With this early version, if I had changed to the next slide and back again, the annotations would have been gone.

Ultimately, I needed something much more sophisticated than that. But I implemented that sophisticated solution later…

Will This Work? How Can I Make it Work?

To implement this feature, I had to implement several things that I never implemented before:

  • I never worked with pointer events before
  • I had to learn how to use the excellent perfect-freehand library
  • I had to design the data structures for the annotations
  • I had to figure out how to exchange new annotation data between the two windows asynchronously
  • And more…

I had no idea how the final solution might look like. I did not even really know how to start.

So, I just tried things. I started writing code.

I created a 100x100 pixel SVG, an array of points, and tried to render them. Tried different ways to handle the pointer events and to add points to the array. Added multiple such arrays, one for each line. Extracted the code to an “overlay” component that I could display above the slides.

I knew that the final solution would not look like that, but I just hacked away, manually testing each step, trusting the TypeScript compiler, trusting that I could just throw away this crappy code should I paint myself into a corner.

A Solution Begins to Form

While hacking, I learned a lot about the problem and the solution. And I got to the point where I could draw on slides and it looked like the real feature: The point where I recorded the animated GIF from the beginning of this blog.

But it was not working properly yet. The annotations were lost when you changed slides. They were not synchronized between the presentation window and the presenter screen. They were nothing like the “final” solution, which works way better:

Animation: Person drawing annotations on slides, they appear both in the presentation and on the presenter screen

I was not there yet, not even close (I recorded that GIF after I had finished everything). But I could now see how to get there. So, I added the data structures I needed and cleaned the code of the overlay a bit.

Even that “final” solution from the second animation is not completely “done” yet: Annotations are not preserved between stopping the presentation and starting it agagin, and they cannot be saved yet. But now, it is good enough to use in a real presentation; Good enough to show it to people and get feedback.

Time to Write Some Tests

Now I had some code that was working (but not finished) and some design that was not perfect, but already not that bad. But I did not have a single test yet.

I talked in the past about how having red tests is extremely important. So, just writing some tests for the existing code that will be “green” from the start is not an option. It should never be an option.

How can I get into the “Red-Green-Refactor” cycle of TDD with all that existing code? For me, there are two ways to achieve it:

  • Throw away the existing code and start again
  • Comment out the existing code and write tests that force you to uncomment lines

While I often use the first tactic (implementing the same functionality a second time is much faster than the first time!), this time I used the second. I commented out most of the code, so much that the remaining code barely compiled, but didn’t work. Then I wrote tests for the existing code, uncommenting the production code to make them work.

Here, for example, for the AnnotationOverlay-React-component:

it('creates a new line on pointerDown', () => { /* ... */ })
it('adds points to current line on pointerMove', () => { /* ... */ })
it('does not add points when pointer is up', () => { /* ... */ })
it('ends the line on pointerUp', () => { /* ... */ })
it('rerenders when lines are updated from the outside', () => { /* ... */ })

I also already refactored a little bit, but right now, there were not that many refactoring opportunities yet.

Expand from there

When all the existing functionality was finally covered by tests, I started to add the missing functionlaity using TDD. Here are, for example, some tests that I wrote to make sure the PresentationWindow communicates correctly with the PresenterScreenWindow:

it('publishes added annotations via the main api', () => { /* ... */ })
it('publishes added lines via the main api', () => { /* ... */ })
it('publishes added point via the main api', () => { /* ... */ })
it('adds line when it receives a line from the main api', () => { /* ... */ })
it('adds point when it receives a point from the main api', () => { /* ... */ })
it('does not publish an added line via the api when the line was received via the api', () => { /* ... */ })
it('does not publish an added point via the api when the point was received via the api', () => { /* ... */ })

At that point, I also found more and more refactoring opportunities.

Conclusion

When you have no idea how the final solution might look like, how to get there and where to start, just hacking some code and trying different things can be faster than researching, planning and designing a solution that you can implement in a test-driven way.

But don’t keep the code like that. For me, there are two main strategies to continue from there:

  • Throw away the existing code and start again
  • Comment out the existing code and write tests that force you to uncomment lines

None of them is inherently better than the other, so I use both from time to time, and which one to use is mostly a gut-decision for me.

By the way, if you want to quickly create presentations by just writing Markdown, give marmota.app a try!