PurelyFunctional.tv Newsletter 325: Tip: don't use a protocol to make testing easier

Free Beginner Workshop

From OO to Clojure Workshop!

Watch my free workshop to help you learn Clojure faster and shift your paradigm to functional.

Issue 325 - May 06, 2019 · Archives · Subscribe

Clojure Tip 💡

don't use a protocol to make testing easier

Warning ⚠️: this may be a controversial tip.

People often use protocols to make testing easier. For instance, they might make a protocol that represents the database. In production, they use a type that implements the protocol that hits the database. But for testing, they use a type that implements the same protocol that uses an in-memory version. Because they are programming to the methods of the protocol, the switch is easy.

I've done this pattern before myself. And I regret it. It's not worth it.

Let me explain.

Until recently, protocols require you to create types. (You can now do dispatch using only metadata, but that is not commonly used in this pattern.) I rarely create new types, so I had to create types just to implement this pattern. They served no production purpose. They were just there to make testing easier.

Further, the protocols themselves only grew with time. As I accessed the database in more and more ways, I had to add new protocol methods. I had to figure out a common denominator for accessing the database and for implementing the database fake. While this has the potential to be a useful exercise, it more often led to poor abstraction. There was rarely any rhyme or reason for the methods. They were just what I needed at the time.

Some might argue that I was just not designing my protocol correctly. And I might agree. But this puts the cart before the horse. Why unnecessarily introduce a place where design is so important?

Some would argue that this pattern allows for extension to other databases. But that extension has never happened. If it did happen, would it fit the protocol I have? Dreams of switching databases as easily as re-implementing a handful of well-chosen methods are unrealistic. And if this is the argument, the razor of YAGNI would shave it off pretty quickly. If I ever need to extend to other databases, I'll implement it then.

Some would argue that this makes your tests harder to write. And they are right. But all it does is shift the burden. Your tests are easier to write, but your production code is harder to write. That is not better.

In the end, protocols are just a layer of indirection. And there is already plenty of indirection between your code and the functions it calls. Define your database access using regular defns. Code as normal. When you go to test, I recommend using with-redefs to install fakes for the functions you need. Writing simple fakes is much easier than implementing a feature-complete version of a database (or whatever system you're swapping out with the protocol pattern). If you can reuse the fake, set up a test fixture that calls with-redefs.

This approach is straightforward. No testing code affects your production code. There is no unnecessary indirection. And functions are easy to grow and evolve with time. Don't follow the protocol-for-testing pattern. It's not worth it.

Do you have a tip you'd like to share? Let me know. Just hit reply.

Currently Recording 🎥

I am currently recording a course called Repl-Driven Development in Clojure. There are a several new lessons ready this week.

  1. Playing with values at the repl contains three tips for working with values directly at the repl.

  2. In Printing values and controlling printing I go over five ways to print and two knobs to control printing.

  3. In Controlling namespace reloading with defonce I explain three different situations where you would want to use defonce to avoid reloading code each time---and a hack to reload the defonce when you do need to.

  4. Navigating namespaces at the repl talks about how to work with exceptions at the repl, and how to read stack traces.

  5. Scientific debugging goes over commands for viewing the current namespace, understanding what's in it, requiring new namespaces, changing namespaces, and removing namespaces. And be sure to catch me messing up the current namespace and having to use the commands we learn to get out of that situation.

  6. Interactive documentation talks about tools for learning about what functions and variables are available in the loaded namespaces.

I've been getting lots of great advice from people watching it. Thanks! This is going to be a much better course because of it. If you watch it, please let me know how it can improve.

There are already 7.25 hours of video. The recording is starting to wind down. I've got a couple more lessons I'd like to record. If there is something in there you would like that you don't see, now is your chance to ask for it.

Of course, members of PurelyFunctional.tv get the course as part of their membership for no extra cost. Just another benefit of being a member.

Check out the course. The first lesson is free.

Brain skill 😎

Embrace "calendar time".

Learning a big skill requires two kinds of time. One is the time you spend actively learning. This is usually measured in hours. Let's call it "clock time". But the other is time your brain spends making new connections and reorganizing to fit the patterns you need. This time is measured in weeks or months, and I call this "calendar time".

You can't just study all your hours in one chunk and expect the same results as if you spread those hours out throughout a year. You need to log a lot of both clock time and calendar time. You can't cram for an exam. Spread the learning out over calendar time.

Clojure Puzzle 🤔

Last week's puzzle

The puzzle in Issue 324 was to implement a simple automaton called Langton's ant.

You can see the submissions here.

I got a few cool images of people visualizing their ants. They kind of all look similar. But Andreas Werner's stood out for being animated:

This week's puzzle

Conway's Game of Life

Conway's Game of Life is a famous cellular automaton. The rules are simple:

  1. Cells are arranged in a square grid.
  2. A cell is either alive (black) or dead (white).
  3. Cells have eight neighbors that surround it.
  4. A dead cell comes to life if it has exactly three live neighbors.
  5. A live cell dies if it has four or more neighbors.
  6. A live cell dies if it has one or zero neighbors.
  7. All other cells continue as they are.

Build this automaton.

Bonus points for allowing an infinite board.

As usual, please send me your implementations. I'll share them all in next week's issue. If you send me one, but you don't want me to share it publicly, please let me know.

Rock on!
Eric Normand