TL;DR: I introduce assert_fields and assert_copy and provide their code. (Updated 2019-12-22 with a new syntax for assert_copy.)

I  used to say "All the words in a test should be about the purpose of the test." I'll probably be exploring some of the ramifications of that slogan throughout the blog. For now, I want to focus on a variant:

All the words I look at in a test should be about my purpose for looking at it.

The "I look at" is because of my new emphasis on scannability. Recall from the previous post that I believe tests should help the reader whose eyes are darting from place to place within a test, searching for an answer to a specific question.

Here are some assertions that improve the scannability of tests involving structs or maps.


Consider code like this:

animal = AnimalT.update_for_success(, params)
assert == "New Bossie"
assert animal.lock_version == 2

A while ago, Steve Freeman and I were pairing, and he reacted badly to code like that. In response, I created an assert_fields function that allows the following:

AnimalT.update_for_success(, params)
|> assert_fields(name: "New Bossie", lock_version: 2)

In addition to chaining the assertion (as in the previous post), I like the way syntax highlighting makes the necessary-but-not-enlightening use of assert_fields fade into the background.


The function tested above produces an updated version of a struct with three kinds of keys:

  1. keys that should have been left alone,
  2. ... keys whose changed value needs to be checked ...
  3. ... and keys whose new value (if any) should be ignored.

A new function, assert_copy, works with assert_fields to handle all three cases in a terse way:

AnimalT.update_for_success(, params)
|> assert_copy(original,
      except:   [name: "New Bossie", lock_version: 2],
      ignoring: [:updated_at])

In the above, :updated_at is the single field whose new value I don't care about. Perhaps that's not right. Perhaps I want to make sure that :updated_at has been increased from its original value. I can do that with...


Actually, I won't write an assertion for :updated_at. It only has a one-second granularity, and I don't want to sleep during tests. Anyway, :updated_at is set by the Ecto machinery, so I'll believe it's correct if other fields have been changed.

So I'll make up an example. It's a test for a bossify function where I require the :tags field to be empty (but I don't care what kind of Enum it is):

test "sample" do
  |> assert_fields(name: "Bossy",
                  tags: &Enum.empty?/1)

Fortunately, Elixir functions generally inspect nicely, so an assertion's failure message can be nice too:

:tags => ["bovine"] fails predicate &Enum.empty?/1

You can also use predicates in the :except arguments to assert_copy.


The version as of this writing is here. There are some features not documented in this post.