Visualizing Test-Driven Design with the Testing Fractal

Visualizing Test-Driven Design with the Testing Fractal
Down Arrow

This was originally a Stride ⚡ Lightning Talk ⚡️ hosted by the Learning Initiative and presented by Elle Mundy.


What is the testing fractal?

The testing fractal is this big secret society, kind of like the Illuminati.

Just kidding! It’s a Sierpinski triangle.

It’s a thought experiment that I came up with when I was taking the baseline training at Stride Consulting, when I first joined. We were talking about outside-in testing. I thought, “Wow, this is kind of like a fractal, isn’t it?”

The testing fractal is a useful way to think about test-driven development (TDD) and how code units integrate with each other. The testing fractal also helps you understand where exactly to write what kinds of tests.

This is not like a hard and fast rule, but it’s more a “thought technology.” It’s a metaphor. It’s not real. It’s all in your mind, you know. Just like the Illuminati.


The testing fractal in the abstract

Each of the tiniest triangles represents a code unit and its respective tests.

Slightly larger triangles, which each encompass three of those smaller triangles, are an integration. And the biggest triangle is the entire system.

hand-drawn Sierpinski triangle. The outermost triangle is labeled “e2e.” The next largest triangles are labeled “integration.” The smallest triangles are labeled “unit.” There are lines connecting the triangles in each set.

In the Sierpinski triangle, you have these upside-down triangles in between the groups of triangles. That’s where the integration and end-to-end tests go.

Let’s go in reverse from there. You do outside-in testing, starting with end-to-end, then integration, then unit. With this, you can start to think about how to break up your code.

Just like in the testing pyramid, the unit test covers only very granular differences between your code paths. Hopefully there will be very few code paths per unit, and the units will be self-contained, doing only small things, preferably only taking values and returning values and having very few, if any, side effects. Or maybe you have one unit that does a side effect, and that’s all it does.

Then your integration tests don’t need to cover any of that granular functionality; they need only to cover how the pieces fit together. All the integration test says is that one unit calls another unit, and that’s it. At this level, we don’t care about what the units do, only that they call each other.

End-to-end tests are the complete view-to-database type of tests; they test that the whole system is basically working together. There should be only a handful of these in your entire system.

That’s just like the Sierpinski triangle. For each feature, there’s only one overarching big triangle. Then you have a few integrations, and then a lot more little units.

The testing fractal in practice

Just like outside-in testing, you would start with a failing end-to-end test, and keep it failing the entire time you’re developing the feature. It’s fine that it’s failing, because red tests are the first step in the red-green-refactor cycle.

Try to satisfy every test case you can at the highest level before moving inward by a level. Maybe the next thing you need is a view, so you write a failing view test.

Maybe that view needs a little bit of data coming from somewhere. So you write a failing test for a controller method, to supply that view with the data.

You keep getting more and more granular until you get to a small enough unit of code that you can get a passing test by writing that little unit. Then you add more tested units next to it, as necessary, to make that controller method test green.

Eventually, you have some well-factored code that supplies the view with all the data it needs, and suddenly your integration test is passing. Before you know it, after repeating all these iterations, if you will, you have a passing end-to-end test.

Congratulations, you have a fully tested feature! Every bit of code you just wrote will be covered by meaningful tests.

Most of these tests will be unit tests that run in milliseconds or microseconds, rather than a whole lot of long-running, repetitive, incomplete end-to-end tests. When you run the entire suite in CI, you’ll find that you’ve added only a few seconds to the entire runtime, not minutes. Over time, even very complex apps will test in just a couple of minutes, not hours, saving a lot of valuable time.


Solving single-responsibility-principle violations with the testing fractal

So what happens if you find that one of your units is violating the single-responsibility principle?

Do another iteration, splitting it out into even tinier triangles, by splitting up a complex unit into a few simpler units. Split it up with TDD. Write a couple of tests for your smaller units, and all of a sudden the tests for the complex unit are now just integration tests.

Now, since that functionality is covered by smaller, less complex, more simple unit tests, the integration tests become redundant. Delete those integration tests.

Deleting tests is one of my favorite things to do. But really my true passion is deleting code, because all code is tech debt.


More reading/viewing and inspirations

Elle Mundy

Elle Mundy

Senior Software Developer

No items found.
green diamond