hachyderm.io is one of the many independent Mastodon servers you can use to participate in the fediverse.
Hachyderm is a safe space, LGBTQIA+ and BLM, primarily comprised of tech industry professionals world wide. Note that many non-user account types have restrictions - please see our About page.

Administered by:

Server stats:

9.5K
active users

Testing Without Mocks: A 🧵.

So a few days ago I released this massive update to my article, "Testing Without Mocks: A Pattern Language." It's 40 pages long if you print it. (Which you absolutely should. I have a fantastic print stylesheet.) I promised a thread explaining what it's all about.

This is the thread. If you're not interested in TDD or programmer tests, you might want to mute me for a bit.

Here's the article I'm talking about: jamesshore.com/v2/projects/tes

www.jamesshore.comJames Shore: Testing Without Mocks: A Pattern Language

2/ First, why bother? Why write 40 pages about testing, with or without mocks?

Because testing is a big deal. People who don't have automated tests waste a huge amount of time manually checking their code, and they have a ton of bugs, too.

The problem is, people who DO have automated tests ALSO waste a huge amount of time. Most test suites are flaky and SLOOOOW. That's because the easy, obvious way to write tests is to make end-to-end tests that are automated versions of manual tests.

3/ Folks in the know use mocks and spies (I'll say "mocks" for short) to write isolated unit tests. Now their tests are fast! And reliable! And that's great!

Except that now their tests have lots of detail about the interactions in the code. Structural refactorings become really hard. Sometimes, you look at a test, and realize: all it's testing... is itself.

Not to mention that the popular way to use mocks is to use a mocking framework and... wow. Have you seen what those tests look like?

4/ So we don't want end-to-end tests, we don't want mocks. What do we do?

The people really REALLY in the know say "bad tests are a sign of bad design." They're right! They come up with things like Hexagonal Architecture and (my favorite) Gary Bernhardt's Functional Core, Imperative Shell. It separates logic from infrastructure so logic can be tested cleanly.

Totally fixes the problem.

For logic.

Anything with infrastructure dependencies… well… um… hey look, a squirrel! (runs for hills)

5/ Not to mention that (checks notes) approximately none of us are working in codebases with good separation of logic and infrastructure, and (checks notes again) approximately none of us have permission to throw away our code and start over with a completely new architecture.

(And even if we did have permission, throwing away code and starting over is a Famously Poor Business Decision with Far-Reaching Consequences.)

6/ So we don't want end-to-end tests, we don't want mocks, we can't start over from scratch... are we screwed? That's it, the end, life sucks?

No.

That's why I wrote 40 pages. Because I've figured out another way. A way that doesn't use end-to-end tests, doesn't use mocks, doesn't ignore infrastructure, doesn't require a rewrite. It's something you can start doing today, and it gives you the speed, reliability, and maintainability of unit tests with the power of end-to-end tests.

7/ I call it (for now, anyway, jury's out, send me your article naming ideas) "Testing With Nullables."

It's a set of patterns for combining narrow, sociable, state-based tests with a novel infrastructure technique called "Nullables."

At first glance, Nullables look like test doubles, but they're actually production code with an "off" switch.

8/ This is as good a point as any to remind everyone that nothing is perfect. End-to-end tests have tradeoffs, mocks have tradeoffs, FCIS has tradeoffs... and Nullables have tradeoffs. All engineering is tradeoffs.

The trick is to find the combination of good + bad that is best for your situation.

9/ Nullables have a pretty substantial tradeoff. Whether it's a big deal or not is up to you. Having worked with these ideas for many years now, I think the tradeoffs are worth it. But you have to make that decision for yourself.

Here's the tradeoff: Nullables are production code with an off switch.

Production code.

Even though the off switch may not be used in production.

11/ The fundamental idea is that we're going to test everything—everything!—with narrow, sociable, state-based tests.

Narrow tests are like unit tests: they focus on a particular class, method, or concept.

Sociable tests are tests that don't isolate dependencies. The tests run everything in dependencies, although they don't test them.

And state-based tests look at return values and state changes, not interactions.

(There's a ton of code examples in the article, btw, if you want them.)

12/ This does raise some questions about how to manage dependencies. Another core idea is "Parameterless Instantiation." Everything can be instantiated with a constructor, or factory method, that takes NO arguments.

Instead, classes do the unthinkable: they instantiate their own dependencies. GASP!

Encapsulation, baby.

(You can still take the dependencies as an optional parameter.)

13/ People ask: "but if we don't use dependency injection frameworks..."

I interrupt: "your code is simpler and easier to understand?" I'm kind of a dick.

They continue, glaring: "...doesn't that mean our code is tightly coupled?"

And the answer is no, of course not. Your code was already tightly coupled! An interface with one production implementation is not "decoupled." It's just wordy. Verbose. Excessively file-system'd.

(The other answer is, sure, use your DI framework too. If you must.)

Marcus Rådell

@jamesshore using a DI framework doesn't reduce coupling in any meaningful way, as you say.

It *hides* coupling and encourages excessive coupling.