Mimic mocks Elixir modules by copying them, then replacing their functions at runtime
A mocking library for Elixir
At a glance
- What is it?
- edgurgel/mimic is an Apache-2.0 Elixir library that lets a test stub, expect or reject calls on a module you do not control, by copying the module in the test helper and swapping the behaviour of the running process. It is careful about the two things that break in Elixir specifically, the compiler optimising away intra module calls, and the fact that global mode changes what concurrent tests see.
- Who is it for?
- Adopt Mimic if your Elixir tests need to fake a collaborator you do not own, such as an HTTP client or a payment gateway, since it swaps the module's functions inside the running process and verifies the calls afterwards.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 5 days ago.
- What is it written in?
- Mainly Elixir, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
You copy the module, then you change it
Mimic is described as a sane way of using mocks in Elixir, and it borrows from both Meck and Mox, with acknowledgements to the two Elixir authors behind them. The model has two halves, and the first is preparation. Modules need to be prepared so that they can be used, and you must first call copy in your test_helper.exs for each module that may have the behaviour changed. The example is short:
Mimic.copy(Calculator)
ExUnit.start()The second half is what makes the approach interesting. Calling copy will not change the behaviour of the module, so merely copying something is harmless. The user must call stub/1, stub/3, expect/4 or reject/1 so that the functions can behave differently. That is the whole contract, and it is worth naming what it buys. The module being faked is typically one you did not write and cannot change, so a behaviour based approach where you must supply your own implementation does not apply, and this library substitutes the implementation at runtime for the test process only. The cost is that the substitution is invisible to the type checker, which is the price of mocking code you do not own.
Stub, expect and reject are three different contracts
The three verbs are not variations on one behaviour, and the difference between them is where most test failures come from. stub/1 will change every module function to throw an exception if called, so it is the strictest option, and the error is a Mimic.UnexpectedCallError with a message naming the function and the calling process. stub/3 changes a specific function to behave differently, and if the function is not called no verification error happens. expect/4 changes a specific function and it works like a queue of operations, it has precedence over stubs, and if it is not called a verification error will be thrown. The queue behaviour is precise. If the same function is called with expect/4 the order will be respected, so three successive expectations on one function return three different results in sequence, and once the queue is exhausted the stub takes over again. expect/4 also takes an optional parameter which is the amount of calls expected, and exceeding it raises, because the expectations are already fulfilled. reject/1 does the opposite of stub, refusing a specific function so that the test can assert that it was never called.
Verification happens at the end of the test unless you say otherwise
With use Mimic, verification of expect/4 function calls is done automatically on test case end. The README notes that verify!/1 can be used in case custom verification timing is required, and the error it raises is a Mimic.VerificationError reading that the expected function was to be invoked a number of times but it has been called fewer times. The automatic timing is right most of the time, because a test that asserts a call happened rarely wants to assert it before the assertion runs, but the explicit form matters in one case, and the README shows it. If a test needs to check the call count in the middle of the test rather than at the end, calling verify!() at that point turns a missing call into an error at the place you are looking, instead of at the end of the case where the failure is further from the cause.
The BEAM compiler will optimise away your stubbed call
This is the sharpest edge in the whole library, and it has a worked example. Using expect/4 on intra-module functions will not work, unless the function is referenced by its fully qualified name. The example defines a module with a mult function and a negation function that calls it, then expects mult to be called, and the call to negation returns the real result rather than the mocked one. The error explains why, because the BEAM optimises this case and jumps directly to the appropriate bytecode. The workaround is short, and the README gives it. To ensure that the stubbed Mimic function is called, it can be referenced by Calculator.mult/2 instead of mult/2. This is not a Mimic problem so much as a property of how the compiler works, and it is the kind of thing that costs an hour if you meet it for the first time, because the mock appears to be ignored rather than broken.
calls/3 records the arguments and then eats them
For assertions about how a collaborator was called, Mimic offers calls/3, which returns a list of args for each call to a stubbed Mimic function, and calls/1 which works the same way but with a capture of the function. The example shows the pattern, where the list is empty before the call and holds the arguments after it. The detail that catches people out is the last line of that section. When calls is called they are popped out of the list of calls, so the next time calls is used it will only return new calls since the last time that calls was used. That makes the function a queue rather than a log, which is the right shape for a test that says what happened since the last checkpoint, and the wrong shape for a test that wants the full history of a call.
Private by default, and global mode has a documented cost
The default mode is private, which means that only the process and explicitly allowed processes will see the different behaviour, and allow/3 permits a different pid to call the stubs and expects from the original process. If you are using Task there is no need to use global mode, because Tasks can see the same expectations and stubs from the calling process. Global mode is set with set_mimic_global in a setup block, and it means that all processes will get the same behaviour defined with expect and stub. The README is blunt about the price, that this option is simpler but tests running concurrently will have undefined behaviour, and it is important to run with async: false. There is also a third option, set_mimic_from_context, which the README suggests instead of choosing between the global and private modes explicitly, and which will be private if async: true and global otherwise.
DSL mode is a second, more expressive surface
The last feature in the README is a mode rather than a function. To use DSL Mode you use Mimic.DSL rather than use Mimic in your test, and DSL Mode enables a more expressive api to the Mimic functionality. The naming suggests the intent, a small language layered over the same primitives, and it is the direction a project of this kind tends to move once the function based API starts to strain, since the pipe chains in the examples, stub then expect then expect on one module, read well but are still function calls in a fixed order. For a team with many mocks in one test file, the DSL is worth trying first, and for a team with a handful, the plain API is probably enough.
Apache-2.0, frequent releases, and a small library
The licence is Apache-2.0, and the repository root is small, with lib/ and test/ directories, mix.exs, mix.lock, a CHANGELOG.md, a usage-rules.md file, a .credo.exs configuration for Credo and a .formatter.exs for the Elixir formatter. The release history shows steady work rather than bursts, with v2.4.0 on 2026-08-22, v2.4.1 on 2026-09-12 and v2.4.2 on 2026-09-18, and the last push on 2026-10-17. The documentation lives on hexdocs.pm/mimic rather than in the repository, and the project describes itself in one line, a mocking library for Elixir. There is one practical note in the installation section worth remembering, because it is the kind of detail that only bites people who configure their suites by hand. If the applications key is defined inside your mix.exs, or you run mix test --no-start, you probably want to add Application.ensure_all_started(:mimic) in your test_helper.exs, because the library has to be running before it can install its substitutions.
Editorial conclusion
Adopt Mimic if your Elixir tests need to fake a collaborator you do not own, such as an HTTP client or a payment gateway, since it swaps the module's functions inside the running process and verifies the calls afterwards. Do not adopt it if your design already supports injecting a fake through a behaviour, because dependency injection gives you the same testability with less machinery, and do not reach for global mode to work around a concurrency problem, since the README is explicit that concurrent tests then have undefined behaviour. Before you use it, remember the two documented traps, that the module must be copied in the test helper, and that a stubbed intra module call has to be referenced by its fully qualified name.
Frequently asked questions
How do I add Mimic to an Elixir project?
Add :mimic to the dependencies in your mix.exs as a test only dependency, for example {:mimic, "~> 2.0", only: :test}. If the applications key is defined in mix.exs, or you run mix test --no-start, you probably want to add Application.ensure_all_started(:mimic) in your test_helper.exs.
What is the difference between stub, expect and reject in Mimic?
stub/1 makes every function of the module throw an exception if called, while stub/3 changes one specific function and raises no error if it is never called. expect/4 changes a specific function, behaves like a queue of operations with precedence over stubs, and raises a verification error if it is not called, optionally with a required call count. reject/1 refuses a specific function so a test can assert it was never called.
Why does my Mimic expectation on an internal function not fire?
Using expect/4 on intra-module functions will not work, unless the function is referenced by its fully qualified name, because the BEAM optimises the case and jumps directly to the appropriate bytecode. The README's fix is to reference the function as Calculator.mult/2 instead of mult/2.
What is the difference between private and global mode in Mimic?
The default private mode means only the process and explicitly allowed processes see the different behaviour, and Tasks already share the expectations of the calling process. Global mode, set with set_mimic_global, gives all processes the same behaviour, which the README says is simpler but leaves tests running concurrently with undefined behaviour, so async: false is required. set_mimic_from_context is private when async is true and global otherwise.
How does Mimic record the calls made to a stubbed function?
calls/3 returns a list of args for each call to a stubbed Mimic function, and calls/1 does the same with a capture of the function. The calls are popped out of the list when calls is used, so a later call returns only the calls made since the previous one.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/edgurgel-mimic)Was this useful?