Stop Putting Mimic.copy in test_helper.exs

Mimic's docs tell you to centralize Mimic.copy calls in test_helper.exs. I don't.

Stop Putting Mimic.copy in test_helper.exs
Photo by Irena Carpaccio / Unsplash

I put them in setup_all blocks, right next to the tests that need them. I think it's better to have them colocated to the tests.

The standard advice

Mimic's README is clear about it:

# test/test_helper.exs
Mimic.copy(Calculator)
Mimic.copy(HTTPClient)
Mimic.copy(PaymentGateway)
ExUnit.start()

One central place, all your mockable modules listed upfront.

Here's what that looks like six months into a real project: a growing list of modules, potentially all of the modules of your app will be there, half of which you're not sure are still needed, and zero indication of which tests actually use which mocks.

What I do instead

# test/my_app/payments/checkout_test.exs
defmodule MyApp.Payments.CheckoutTest do
  use ExUnit.Case, async: true

  setup_all do
    Mimic.copy(PaymentGateway)
    :ok
  end

  # tests that actually mock PaymentGateway...
end

The mock declaration lives with the tests that use it. Open the file, see the dependency.

Why this matters more now

This was already a good idea before LLMs started writing our tests. It's way more important now.

When an LLM generates a test file, it needs to produce something self-contained. A test that silently depends on a line in test_helper.exs will fail with a confusing error the first time you run it. Then you (or the LLM) have to figure out that the fix isn't in the test file at all, it's in a global setup file that the test never references, it's implicit.

With setup_all, the LLM generates a complete, working test. No second file to touch.

This goes beyond LLMs though. Every time you force someone to jump to another file to understand what's happening, you're making the code harder to reason about. test_helper.exs becomes this huge list of modules with no context about why they're there or who uses them. It's dead weight.

"But what about performance?"

I checked. There's no hit. Mimic.copy/1 is idempotent. The first call marks the module in a MapSet inside a GenServer. Every subsequent call for the same module just hits that check and returns :ok. The expensive BEAM bytecode work only happens once, lazily, on the first stub or expect call. I mean, there's the hit to check if the module is in the MapSet, but I'm assuming that's negligible.

So if two test files both call Mimic.copy(PaymentGateway) in their setup_all, the second one is a no-op.

The point

Your test files should be self-contained. If a test needs a mock, it should say so, right there, not in a file 2 or 3 directories up.


George Guimarães builds agentic commerce infrastructure at New Generation. Previously: Principal Engineer at a unicorn fintech, co-founder of Plataformatec (acqui-hired by Nubank).


Great! You’ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to George Guimarães..

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.