Library / SDK
eproxus/meck avatar
eproxus/meck

meck: mocking Erlang modules by recompiling them

A mocking library for Erlang

840 stars230 forksErlangApache-2.0

At a glance

What is it?
meck is a mocking library for Erlang, and it works the only way it can: by recompiling a module into the runtime's global namespace and swapping it out. That mechanism is the whole story, and the README is admirably honest about what it means, including the list of modules you will never be able to mock.
Who is it for?
Adopt meck if you maintain Erlang or Elixir code that needs its dependencies replaced in tests, and you are comfortable with a mechanism that changes modules in the running virtual machine rather than intercepting calls. Do not adopt it if your code calls into native implemented functions or into the runtime's own core modules, because the README lists those as impossible or dangerous to mock.
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 8 days ago.
What is it written in?
Mainly Erlang, 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

The mechanism is a recompile, which explains everything

meck is described simply as a mocking library for Erlang, and the README is clear that it works by recompiling and reloading modules in the global Erlang module namespace.

That single sentence explains both the power and the limits of the library. Because a mock is a real module loaded into the runtime, it can do things that a call interceptor cannot: it can be a module that never existed, it can have a different set of exported functions, it can be unloaded and the original restored. The README lists automatic mock creation for existing modules when you mock a function, and protection against mocking modules that do not exist, which you can turn off with the non_strict option.

The consequence is that a mock is not local to a test process in the way people expect. Replacing a module affects the whole Erlang virtual machine and any running processes using that module, which the README says means certain modules cannot be mocked or will cause trouble. It also means the mock lifecycle has to be managed carefully, and the library has options for that, including automatic unloading when the creating process crashes, which you can disable with the no_link option.

The library is Apache-2.0 licensed, published on Hex.pm as meck, with its documentation on HexDocs, and the header carries a badge for the minimum supported Erlang version, which is maintained in the continuous integration workflow file. The releases are v1.1.0 in October 2025, v1.1.1 in March 2026 and v1.2.0 in May 2026, with the last push on 2026-10-15, so this is a mature library still receiving releases.

Expectations are objects you can change while the test runs

The basic cycle is four calls, and the shell example in the README shows them in full.

erlang
1> meck:new(dog, [non_strict]). % non_strict is used to create modules that don't exist
ok
2> meck:expect(dog, bark, fun() -> "Woof!" end).
ok
3> dog:bark().
"Woof!"
4> meck:validate(dog).
true
5> meck:unload(dog).
ok

The validation step is the interesting one, because it is what turns a mock into an assertion. After the test body, meck:validate asks whether the mock was used the way it was meant to be, and the expectation can be a single clause, several clauses, or a matcher rather than an exact argument list.

The feature list is worth reading as a list of testing decisions rather than as API surface. Expectations can be added, listed, merged with the merge_expects option and deleted while the test is running, which means a test can change the mock's behaviour between two calls under test. Return values can be dynamic, using sequences and loops of static values, so a call can return a different answer on each invocation. The default return value for every function of the mocked module can be customised with the stub_all option, which is how you mock a module whose surface you do not care about. And exceptions can be declared as intentional, in which case validation still passes, which is how you test code that is supposed to handle a failure.

The contrast case is the normal exception. The README shows a function that only accepts a height of three or less, so calling it with a larger value raises a function clause error and the validation returns false. The distinction between an anticipated exception and an unanticipated one is exactly what makes the mock useful as a test assertion rather than as a stub.

Call history is treated as a first class feature

The second feature group is about what the mock remembers, and it is more than most mocking libraries offer.

The history is complete, showing calls, return values and exceptions, and you can capture individual argument values from specific calls rather than only counting them. You can wait for a specific call to reach the mock, with a timeout, which is the difference between a test that polls and a test that blocks. The history can be reset between cases, and recording can be turned off entirely with the no_history option for tests where the overhead matters.

The no_history option is the one to note for performance work. Call recording on a hot path in a suite can dominate the runtime, and the library makes that a deliberate choice rather than a default you have to work around.

The lifecycle group has a similar pragmatism. Mocks that were not called correctly are invalidated, so a test cannot pass by never touching the mock. Sticky modules, which the runtime does not allow to be reloaded, are handled with the unstick option, which is a small piece of pragmatism that makes the library work on code it otherwise could not touch. And the cover data of the original module is backed up and restored automatically, with the option to disable that for passthrough calls, so instrumenting a module for coverage does not leave the mock in a state that breaks the coverage report.

There is also batch operation across several modules at once, which is a small feature that matters in a suite that mocks three or four collaborators of one subsystem.

Adding meck is one line in rebar.config

Installation is the shortest part of the story, because the library is built for Rebar 3, the standard Erlang build tool.

You add it to the test dependencies in your rebar.config, and the recommended form puts it in a test profile so it never reaches your production dependencies:

erlang
{profiles, [{test, [{deps, [meck]}]}]}.

For the library itself, the build is a compile, the tests are an EUnit run and the documentation is an edoc generation:

sh
rebar3 compile
sh
rebar3 eunit
sh
rebar3 edoc

The test output section is a small but useful piece of documentation, because running EUnit directly produces output that looks alarming. Two things might seem wrong: warnings emitted by the cover tool, and an exception printed by SASL. The README says both are expected, given how Erlang currently prints errors, and that the line to look for is the one reporting that all the tests passed.

That kind of note is what you find in a project with a long history, and the repository bears the marks of one: a Travis configuration left in the tree next to a GitHub Actions workflow, a lint configuration for the Erlang language server, a dialyzer ignore file, a change log, a code of conduct, a notice file beside the licence, and a scripts directory. The test suite itself sits in a test directory beside the source, which is the layout Rebar expects.

The caveats section is the one to read before you choose it

The README has a caveats section, and it is the most useful part of the document for an evaluator.

The first caveat is the global namespace problem, and it is stated without hedging. Meck has trouble mocking certain modules since it works by recompiling and reloading modules in the global namespace, and replacing a module affects the whole virtual machine and any running processes using that module.

The consequences are concrete. A module that is used by running processes, or that includes native implemented functions, will be hard or impossible to mock. The README is honest about the tone of the odds, saying you may be lucky and it could work until it breaks one day. And then there is the list of modules that are problematic or impossible, which is the most useful sentence in the whole project for a reader deciding whether to use the library.

The second caveat is local functions, and it is the one that produces a test that passes for the wrong reason. A Meck expectation set up for a function does not apply to the module-local invocation of that function inside the mocked module. The README gives a module with three functions where one calls another locally and another calls it fully qualified, and shows that after mocking, the fully qualified call returns the new value while the local call is unchanged. So the mock behaves differently depending on how the function is called, and a test that exercises only the local path can appear to pass while the external behaviour is untested.

Neither caveat is fixable by configuration. They are properties of how a language without a mocking framework can be mocked at all, and a project that accepts them gets a mocking library with runtime expectations, call history and validation, which is a lot for a mechanism this invasive.

Editorial conclusion

Adopt meck if you maintain Erlang or Elixir code that needs its dependencies replaced in tests, and you are comfortable with a mechanism that changes modules in the running virtual machine rather than intercepting calls. Do not adopt it if your code calls into native implemented functions or into the runtime's own core modules, because the README lists those as impossible or dangerous to mock. Before you use it in a suite, read the caveats section in full, check the module-local call case, which is the one that produces a test that passes for the wrong reason, and remember that the mock affects every process in the virtual machine while it is loaded.

Frequently asked questions

How does meck mock Erlang modules?

By recompiling and reloading modules in the global Erlang module namespace. That is why a mock can be created for a module that does not exist, with the non_strict option, and why replacing a module affects the whole virtual machine and any running processes using it.

How do I add meck to an Erlang project?

Through Rebar 3, by adding it to the test dependencies in rebar.config, which the README shows as a test profile entry so that it stays out of your production dependencies. The library itself is built with rebar3 compile, tested with rebar3 eunit and documented with rebar3 edoc.

What does meck:validate check?

It checks that the mock was used the way it was meant to be, and it fails if an unexpected exception was raised in the mocked function. Exceptions that the test declares as intentional are anticipated by the library, and validation still passes in that case, which is how you test code that is supposed to handle a failure.

Which modules can meck not mock?

The README lists the erlang, supervisor, gen_ family, os, crypto, compile, global and timer modules as problematic or impossible, and notes that modules used by running processes or with native implemented functions are hard or impossible to mock. The timer module is possible to mock but is used by some test frameworks, including Elixir's ExUnit.

Does a meck expectation apply to module-local calls?

No. An expectation set up for a function does not apply when that function is called from inside the mocked module, only when it is called in a fully qualified way. The README shows a module where one function calls another locally and another calls it fully qualified, and the local call keeps the original behaviour after the mock is installed.

Official sources

  1. eproxus/meck on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/eproxus-meck.svg)](https://hysenlabs.com/projects/eproxus-meck)

Was this useful?