View Source Mneme (Mneme v0.0.3)

/ni:mi:/ - Snapshot testing for regular ol' Elixir code.

Mneme helps you write tests using auto_assert/1, a "replacement" for ExUnit's assert. The difference between the two is simple: with auto_assert, you write an expression and Mneme updates the test with an assertion based on the runtime value.

For example, let's say you've written a test for a function that removes even numbers from a list:

test "drop_evens/1 should remove all even numbers from an enum" do
  auto_assert drop_evens(1..10)

  auto_assert drop_evens([])

  auto_assert drop_evens([:a, :b, 2, :c])
end

The first time you run this test, you'll receive three prompts (complete with diffs) asking if you'd like to update each of these expressions. After accepting, your test is re-written:

test "drop_evens/1 should remove all even numbers from an enum" do
  auto_assert [1, 3, 5, 7, 9] <- drop_evens(1..10)

  auto_assert [] <- drop_evens([])

  auto_assert [:a, :b, :c] <- drop_evens([:a, :b, 2, :c])
end

The next time you run this test, you won't receive a prompt and these will act (almost) like any other assertion. (See auto_assert/1 for details on the differences from ExUnit's assert.)

setup

Setup

# 1) add :mneme to your :import_deps in .formatter.exs
[
  import_deps: [:mneme],
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

# 2) start Mneme right after you start ExUnit in test/test_helper.exs
ExUnit.start()
Mneme.start()

# test/my_test.exs
defmodule MyTest do
  use ExUnit.Case, async: true

  # 3) use Mneme wherever you use ExUnit.Case
  use Mneme

  test "arithmetic" do
    # 4) use auto_assert instead of ExUnit's assert - run this test
    #    and delight in all the typing you don't have to do
    auto_assert 2 + 2
  end
end

match-patterns

Match patterns

Mneme tries to generate match patterns that are equivalent to what a human (or at least a nice LLM) would write. Basic data types like strings, numbers, lists, tuples, etc. will serialize as you would expect.

Some values, however, do not have a literal representation that can be used in a pattern match. Pids are such an example. For those, guards are used:

auto_assert self()

# after running the test and accepting the change
auto_assert pid when is_pid(pid) <- self()

Pinned variables are also supported and are used if possible for these kinds of values that do not have a convenient literal representation:

me = self()
auto_assert self()

# after running the test
auto_assert ^me <- self()

non-exhaustive-list-of-special-cases

Non-exhaustive list of special cases

  • Non-serializable values like pids, refs, ports, and functions generate guards (unless the exact value is present locally, in which case a pin is used).

  • Date and time values serialize to their sigil representation.

  • Struct patterns only include fields that are different from the struct defaults.

  • Structs defined by Ecto schemas exclude primary keys and auto generated fields like :inserted_at and :updated_at when serialized.

configuration

Configuration

There are a few controls that can be used to change Mneme's behavior when it runs auto-assertions. These can be set at the module-level by passing options to use Mneme, the describe level using the @mneme_describe attribute, or the test level using the @mneme attribute. For instance:

defmodule MyTest do
  use ExUnit.Case

  # reject all changes to auto-assertions by default
  use Mneme, action: :reject

  test "this test will fail" do
    auto_assert 1 + 1
  end

  describe "some describe block" do
    # accept all changes to auto-assertions in this describe block
    @mneme_describe action: :accept

    test "this will update without prompting" do
      auto_assert 2 + 2
    end

    # prompt for any changes in this test
    @mneme action: :prompt
    test "this will prompt before updating" do
      auto_assert 3 + 3
    end
  end
end

See __using__/1 for a description of available options.

requirements

Requirements

Mneme currently requires that you use the Elixir formatter in your tests and will reformat the entire file when it updates an assertion. If you do not use the formatter, this may cause Mneme to change the formatting of unrelated tests (though it shouldn't change the behavior).

Link to this section Summary

Functions

Sets up Mneme configuration for this module and imports auto_assert/1.

Generate or run an assertion.

Starts Mneme to run auto-assertions as they appear in your tests.

Link to this section Functions

Link to this macro

__using__(opts)

View Source (macro)

Sets up Mneme configuration for this module and imports auto_assert/1.

options

Options

Options passed to use Mneme can be overriden in describe blocks or for individual tests. See the "Configuration" section in the module documentation for more.

  • :action - The action to be taken when an auto-assertion updates. Actions are one of :prompt, :accept, or :reject. If CI=true is set in environment variables, the action will always be :reject. The default value is :prompt.

  • :target - The call output when an auto-assertion updates. If :assert, auto-assertions will be rewritten with ExUnit's assert when they update. The default value is :auto_assert.

example

Example

defmodule MyTest do
  use ExUnit.Case
  use Mneme # <- add this

  test "..." do
    auto_assert ...
  end
end
Link to this macro

auto_assert(body)

View Source (macro)

Generate or run an assertion.

auto_assert generates assertions when tests run, issuing a terminal prompt before making any changes (unless configured otherwise).

auto_assert [1, 2] ++ [3, 4]

# after running the test and accepting the change
auto_assert [1, 2, 3, 4] <- [1, 2] ++ [3, 4]

If the match no longer succeeds, a warning and new prompt will be issued to update it to the new value.

auto_assert [1, 2, 3, 4] <- [1, 2] ++ [:a, :b]

# after running the test and accepting the change
auto_assert [1, 2, :a, :b] <- [1, 2] ++ [:a, :b]

Prompts are only issued if the pattern doesn't match the value, so that pattern can also be changed manually.

# this assertion succeeds
auto_assert [1, 2, | _] <- [1, 2] ++ [:a, :b]

differences-from-exunit-assert

Differences from ExUnit assert

The auto_assert macro is meant to match assert as closely as possible. In fact, it generates ExUnit assertions under the hood. There are, however, a few small differences to note:

  • Pattern-matching assertions use the <- operator instead of the = match operator. Value-comparison assertions still use == (for instance, when the expression returns nil or false).

  • Guards can be added with a when clause, while assert would require a second assertion. For example:

    auto_assert pid when is_pid(pid) <- self()
    
    assert pid = self()
    assert is_pid(pid)
  • Bindings in an auto_assert are not available outside of that assertion. For example:

    auto_assert pid when is_pid(pid) <- self()
    pid # ERROR: pid is not bound

    If you need to use the result of the assertion, it will evaluate to the expression's value.

    pid = auto_assert pid when is_pid(pid) <- self()
    pid # pid is the result of self()

Starts Mneme to run auto-assertions as they appear in your tests.

This will almost always be added to your test/test_helper.exs, just below the call to ExUnit.start():

# test/test_helper.exs
ExUnit.start()
Mneme.start()