Expect.Matchers (Expect v3.1.0)

View Source

A matcher is responsible for providing expect/2 three things

  1. the name for the matcher
  2. the value that was matched against (optional)
  3. a function to invoke with the given value to expect()

Expect will invoke the matcher's function, and if it returns false or an error tuple, it will construct an error message using the name and the value, and fail the test.

For example, given a matcher that returns %CustomMatcher{name: "end with", expected: "cool", fn: fn given -> String.ends_with?(given, "cool") end}, we could expect the following result

expect("literal fire", to: end_with("cool"))
# raises an error with message "Expected '"literal fire"' to end with '"cool"', but it did not"

Alternatively, a matcher that returns %CustomMatcher{name: "be greater than", expected: 5, fn: fn given -> given > 5 end} would have the following result

expect(0, to: be_greater_than(5))
# raises an error with message "Expected '0' to be greater than '5'"

Custom Failure Messages

By default, when a matcher fails, the error message will use the name of the matcher, and the value matched against.

eg: given a matcher returns %CustomMatcher{name: "start with", expected: "gravy", fn: fn given -> String.starts_with?(given, "gravy") end} we could expect the following line to fail

expect("groovy train", to: start_with("gravy"))
# fails with message "Expected '"groovy train"' to start with '"gravy"'"

If you are writing a matcher that doesn't compare the given value against something else, you can omit the actual key (only the name and fn keys are required)

def start_with_a, do: %CustomMatcher{name: "start with the letter 'A'", fn: fn given -> String.starts_with?(given, "A") end}`

expect("algebra", to: start_with_a())
# passes

expect("monotonic", to: start_with_a())
# fails with message "Expected '"monotonic"' to start with the letter 'a'"

Custom error messages

Sometimes, a matcher might receive input that it is incapable of dealing with. A matcher that wants to verify if a string has a certain prefix would be unable to handle input that is not a string. This can be handled by returning an error tuple from a matcher

def end_with(suffix) do: %CustomMatcher{name: "end with", expected: suffix, fn: &verify_suffix(&1, suffix)}

defp verify_suffix(given, suffix) when is_binary(given), do: String.ends_with?(given, suffix)
defp verify_suffix(given, suffix), do: {:error, "end with '#{suffix}', but it was not a string"}

Summary

Functions

Verifies that expected is either the atom :error or an error tuple

Verifies that expected is an empty list, map, or tuple

Verifies that expected is strictly greater than value, using >.

Verifies that expected is strictly less than value, using <.

Verifies that expected is nil

Verifies that expected is a falsy value -- either nil or false

Verifies that the provided value is in the given Enum

Verifies that expected is equal to value, using ==.

Verifies that expected is either a List or String with the given length

Matches expected against the provided regular expression using Regex.match?

Verifies that the given value matches against the given pattern.

Types

t()

@type t() :: %Expect.Matchers.CustomMatcher{
  expected: matched_against :: any(),
  fn: (given :: any() ->
         %Expect.Matchers.Result{succeeded?: term()}
         | %Expect.Matchers.ErrorResult{error: term()}),
  name: matcher_name :: String.t()
}

Functions

be_an_error()

@spec be_an_error() :: t()

Verifies that expected is either the atom :error or an error tuple

be_empty()

@spec be_empty() :: t()

Verifies that expected is an empty list, map, or tuple

be_greater_than(value)

Verifies that expected is strictly greater than value, using >.

be_in_range(range)

@spec be_in_range(Range.t()) :: t()

be_less_than(value)

Verifies that expected is strictly less than value, using <.

be_nil()

@spec be_nil() :: t()

Verifies that expected is nil

be_truthy()

@spec be_truthy() :: t()

Verifies that expected is a falsy value -- either nil or false

contain(value)

@spec contain(any()) :: t()
@spec contain([{:only, any()}]) :: t()

Verifies that the provided value is in the given Enum

Works with any Enumerable type (eg: Lists, Keyword lists, Ranges, Maps, etc)

If you want to verify that the enumerable ONLY contains the one value then use :only

expect([1], to: contain(only: 1))

equal(value, opts \\ [])

@spec equal(any(), Keyword.t()) :: t()

Verifies that expected is equal to value, using ==.

If you want to verify strict equality with === then use strict: true

expect(1, to: equal(1.0, strict: true))

have_length(expected_length)

@spec have_length(non_neg_integer()) :: t()

Verifies that expected is either a List or String with the given length

match_regex(regex)

@spec match_regex(Regex.t()) :: t()

Matches expected against the provided regular expression using Regex.match?

pattern_match(expected)

(macro)

Verifies that the given value matches against the given pattern.

  expect({:ok, _msg}, to: pattern_match({:ok, "this is fine"}))

  expect({:ok, _msg}, to: pattern_match({:error, "this is NOT fine !!!"}))
  # raises an error

  # you can also pass in variables for the pattern
  result = {:error, "oh gods everything is on fire"}
  expect({:error, _reason}, to: pattern_match(result))

  # nb : you do not want to pass in variables for the given value
  # this test will never fail, as it effectively re-assigns the variable `whoops`
  whoops = :ok
  expect(whoops, to: pattern_match(:error))

## Binding variables

You can bind variables during a pattern match and use that for later assertions. This is frequently helpful when you have some large data structure and assert several distinct facts about it

  my_data = %{large: %{structure: %{containing: %{a_list: ["cool"]}}}}

  expect(%{large: %{structure: %{containing: %{a_list: list}}}}, to: pattern_match(my_data))
  expect(list, to: contain("cool"))