/ni:mi:/ - Snapshot testing for Elixir ExUnit

🎥 Video Demo

https://user-images.githubusercontent.com/503938/227819477-c7097fbc-b9a4-44a1-b3ea-f1b420c18799.mp4


Note: This README tracks the main branch. See the HexDocs linked below for documentation for the latest release.


Hex.pmDocsCI

Snapshot tests assert that some expression matches a reference value. It's like a regular assert, except that the reference value is generated for you by Mneme.

Mneme follows in the footsteps of existing snapshot testing libraries like Insta (Rust), expect-test (OCaml), and assert_value (Elixir). Instead of simple value or string comparison, however, Mneme focuses on pattern matching.

A brief example

Let's say you're working on 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

Notice that these assertions don't really assert anything yet. That's okay, because the first time you run mix test, Mneme will generate the patterns and prompt you with diffs. When you accept them, your test is updated for you:

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 your tests, you won't receive prompts (unless something changes!), and these auto-assertions will act like a normal assert. If things do change, you're prompted again and can choose to accept and update the test or reject the change and let it fail.

A brief tour

To see Mneme in action without adding it to a project, you can download and run the standalone tour:

curl -o tour_mneme.exs https://raw.githubusercontent.com/zachallaun/mneme/main/examples/tour_mneme.exs
elixir tour_mneme.exs

Quick start

  1. Add :mneme do your deps in mix.exs:

    defp deps do
      [
        {:mneme, ">= 0.0.0", only: :test}
      ]
    end
  2. Add :mneme to your :import_deps in .formatter.exs:

    [
      import_deps: [:mneme],
      inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
    ]
  3. Start Mneme right after you start ExUnit in test/test_helper.exs:

    ExUnit.start()
    Mneme.start()
  4. Add use Mneme wherever you use ExUnit.Case:

    defmodule MyTest do
      use ExUnit.Case, async: true
      use Mneme
    
      test "arithmetic" do
        # use auto_assert instead of ExUnit&#39;s assert - run this test
        # and delight in all the typing you don&#39;t have to do
        auto_assert 2 + 2
      end
    end

Pattern matching

Snapshot testing is a powerful tool, allowing you to ensure that your code behaves as expected, both now and in the future. However, traditional snapshot testing can be brittle, breaking whenever there is any change, even ones that are inconsequential to what is being tested.

Mneme addresses this by introducing Elixir's pattern matching to snapshot testing. With pattern matching, tests become more flexible, only failing when a change affects the expected structure. This allows you to focus on changes that matter, saving time and reducing noise in tests.

To facilitate this, Mneme generates the patterns that you were likely to have written yourself. If a value contains some input variable in its structure, Mneme will try to use a pinned variable (e.g. ^date). If the value is an Ecto struct, Mneme will omit autogenerated fields like timestamps that are likely to change with every run. And if Mneme doesn't get it quite right, you can update the pattern yourself -- you won't be prompted unless the pattern no longer matches.

Generated 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 be 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()

# generates:

auto_assert pid when is_pid(pid) <- self()

Additionally, local variables can be found and pinned as a part of the pattern. This keeps the number of hard-coded values down, reducing the likelihood that tests have to be updated in the future.

test "create_post/1 creates a new post with valid attrs", %{user: user} do
  valid_attrs = %{title: "my_post", author: user}

  auto_assert create_post(valid_attrs)
end

# generates:

test "create_post/1 creates a new post with valid attrs", %{user: user} do
  valid_attrs = %{title: "my_post", author: user}

  auto_assert {:ok, %Post{title: "my_post", author: ^user}} <- create_post(valid_attrs)
end

In many cases, multiple valid patterns will be possible. Usually, the "simplest" pattern will be selected by default when you are prompted, but you can cycle through the options as well.

Non-exhaustive list of special cases

Formatting

Mneme uses Rewrite to update source code, formatting that code before saving the file. Currently, the Elixir formatter and FreedomFormatter are supported. If you do not use a formatter, the first auto-assertion will reformat the entire file.

Continuous Integration

In a CI environment, Mneme will not attempt to prompt and update any assertions, but will instead fail any tests that would update. This behavior is enabled by the CI environment variable, which is set by convention by many continuous integration providers.

export CI=true

Editor support

Guides for optional editor integration can be found here:

Acknowledgements

Special thanks to:

Configuration

See the full module documentation for configuration options.