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.