View Source Machete (Machete v0.2.3)

Machete provides ergonomic match operators to help make your ExUnit tests more literate

The easiest way to explain Machete is to show it in action:

defmodule ExampleTest do
  use ExUnit.Case
  use Machete

  test "example test" do
    response = %{
      id: 1,
      name: "Moe Fonebone",
      is_admin: false,
      created_at: DateTime.utc_now()
    }

    assert response ~> %{
      id: integer(positive: true),
      name: string(),
      is_admin: false,
      created_at: datetime(roughly: :now, time_zone: :utc)
    }
  end
end

At its heart, Machete provides the following two things:

  • A new ~> operator (the 'squiggle arrow') that does flexible matching of its left operator with its right operator
  • A set of parametric matchers such as string() or integer() which can match against general types

These building blocks let you define test expectations that can match data against any combination of literals, variables, or parametrically defined matchers

When your matches fail, Machete provides useful error messages in ExUnit that point you directly at any failing matches using jq syntax

matching-literals-variables

Matching literals & variables

Machete matches directly against literals & variables. The following examples will all match:

# Builtin type literals all match themselves:
assert 1 ~> 1
assert "abc" ~> "abc"

# Comparison is strict, using === semantics:
refute 1.0 ~> 1
refute "123" ~> 123

# Variables 'just work' everywhere; no pinning required!
a_number = 1
assert a_number ~> 1
assert 1 ~> a_number
assert a_number ~> a_number

# Date-like types are compared using the relevant `compare/2` function:
assert ~D[2021-02-01] ~> ~D[2021-02-01]

# Regexes match using =~ semantics:
assert "abc" ~> ~r/abc/

# Structs can be matched on a subset of their fields:
assert %User{name: "Moe"} ~> struct_like(User, name: string())

type-based-matchers

Type-based matchers

Machete comes with parametric matchers defined for a variety of types. Many of these matchers take optional arguments to further refine matches (for example, integer(positive: true) will match positive integers). For details, see the documentation of specific matchers below. Thmache following matchers are defined by Machete:

collection-matchers

Collection matchers

Collections can be matched as literals, with their contents being recursively matched. This usage requires knowing the exact shape of the collection up front, and may not always be suitable. For cases where you may need more flexible collection matching, Machete provides the following matchers:

  • in_any_order() matches lists in any order
  • indifferent_access() matches maps, considering similar atom and string keys to be equivalent
  • list() matches lists, with optional constraints on element type & list length
  • map() matches maps, with optional constraints on key and value types & map size
  • subset() matches maps against its intersection with a (possibly larger) map
  • superset() matches maps against its intersection with a (possibly smaller) map

miscellaneous-matchers

Miscellaneous matchers

  • all() matches the value against a set of matchers, requiring all of them to match
  • any() matches the value against a set of matchers, requiring at least one of them to match
  • maybe() matches using a specified matcher, but also matches nil
  • none() matches the value against a set of matchers, requiring none of them to match

write-your-own-matchers

Write your own matchers

Implementing your own matchers is easy! Behind the scenes, parametric matchers are plain functions which return structs conforming to the Machete.Matchable protocol. You can implement your own matchers by doing the same; a good place to start would be to look at something like the falsy matcher as a starting point.

For more adhoc matchers, you can also define a function which returns the output of an existing matcher. For example, if you wanted to define a matcher named tags() which would match a (possibly empty) list of non-empty strings without whitespace, you could do so like so:

def tags(), do: list(elements: string(empty: false, whitespace: false))

assert %{
  tags: ["cool", "awesome", "not_lame"]
} ~> %{
  tags: tags()
}

This allows you to easily DRY up your test expectations, keeping a centralized notion of what various formats in your data look like

Link to this section Summary

Functions

Brings the ~> and ~>> operators into scope, along with assert/1 and refute/1 macros to be aware of them. Also brings the set of parameteric matchers listed above into scope. Typical use of this is to use Machete at the top of your ExUnit tests, taking care to place this after any use ExUnit... calls to ensure that the relevant assert/1 and refute/1 macros are properly scoped

Link to this section Functions

Link to this macro

__using__(opts)

View Source (macro)

Brings the ~> and ~>> operators into scope, along with assert/1 and refute/1 macros to be aware of them. Also brings the set of parameteric matchers listed above into scope. Typical use of this is to use Machete at the top of your ExUnit tests, taking care to place this after any use ExUnit... calls to ensure that the relevant assert/1 and refute/1 macros are properly scoped