Machete (Machete v0.3.11)
View SourceMachete 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()
orinteger()
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
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
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. The
following matchers are defined by Machete:
atom()
matches atomsboolean()
matches boolean valuesdate()
matchesDate
instancesdatetime()
matchesDateTime
instancesfalsy()
matches falsy valuesfloat()
matches float valuesinteger()
matches integer valuesiso8601_datetime()
matches ISO8601 formatted stringsjson()
matches JSON formatted structuresis_a()
matches against a struct typenaive_datetime()
matchesNaiveDateTime
instancespid()
matches process IDsport()
matches Erlang portsreference()
matches Erlang referencesstring()
matches UTF-8 binariesstruct_like()
matches structs based on type and a set of fieldsterm()
matches any term (including nil)time()
matchesTime
instancestruthy()
matches truthy valuesunix_time()
matches integers that represent unix time
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 orderindifferent_access()
matches maps, considering similar atom and string keys to be equivalentlist()
matches lists, with optional constraints on element type & list lengthmap()
matches maps, with optional constraints on key and value types & map sizesubset()
matches maps against its intersection with a (possibly larger) mapsuperset()
matches maps against its intersection with a (possibly smaller) map
Miscellaneous matchers
all()
matches the value against a set of matchers, requiring all of them to matchany()
matches the value against a set of matchers, requiring at least one of them to matchmaybe()
matches using a specified matcher, but also matches nilnone()
matches the value against a set of matchers, requiring none of them to match
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
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
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