marick

marick

Scannable map or struct assertions

TL;DR: I introduce assert_fields and assert_copy and provide their code. Original blog post here.


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.

assert_fields

Consider code like this:

animal = AnimalT.update_for_success(original.id, params)
assert animal.name == "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(original.id, 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.

assert_copy

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

  • keys that should have been left alone,
  • … keys whose changed value needs to be checked …
  • … 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(original.id, 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…

predicates

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
  bossify("Bossy")
  |> assert_fields(name: "Bossy",
                   tags: &Enum.empty?/1)
end

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

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

Source

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

Most Liked

devonestes

devonestes

Interesting! I actually have a library that I think would suit this case for you (and hopefully provide really great error messages for you as well). Check it out here: assertions | Hex

Is there something in your functions that you can’t easily do with that library? If so, I’d love to add it there. I envision that library as (eventually) a big collection of common assertion abstractions like these ones you mention.

Schultzer

Schultzer

I hope you are aware that you can leverage pattern matching here, so you could simply do all of this in one line.

assert %Animal{name: "New Bossie", lock_version: 2} = AnimalT.update_for_success(original.id, params)
hauleth

hauleth

I find it easier to read something like that:

assert %{
    name: "New Bossie",
    lock_version: 2
  } = AnimalT.update_for_success(original.id, params)

This also makes “predicates” simpler:

assert %{
    name: "Bossy",
    tags: tags
  } = bossify("Bossy")
assert Enum.empty?(tags)

Last Post!

marick

marick

Meta:

I was an early adopter of Ruby. In one of the early RubyConfs, maybe the first one, Matz said:

Perl has many, many ways to do something.
Python has One True Way.
Ruby has more than one way, but some are better than others.

An oddity is that what’s better can depend on circumstances, which is why providing more than one way is better.

I think pattern matching is the bee’s knees. But it can lead to code that isn’t easily to grasp at a glance, at least for some people.

For example, I had tests that had to dissect changesets to look at subcomponents. I find assertions like this more easy to grasp:

I think it’s important to note that:

  1. people who prefer a pattern-matching style can still use it.
  2. those people can easily understand assert_fields style.

I think it’s OK to offer people options and let them decide, even if they decide differently than I would.

Where Next?

Popular in Discussions Top

JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 54996 245
New
MarioFlach
Hello, I want to share a project I’ve been working on for a while: https://github.com/almightycouch/gitgud Background Some time ago I ...
New
tmbb
This is a post to discuss the new Phoenix LiveView functionality. From Chris’s talk, it appears that they generate all HTML on the serve...
342 18595 126
New
tomekowal
Hey guys! I want to create a toy project that shows a chart of temperature over time and updates every 5 seconds. I feel LiveView is per...
New
Fl4m3Ph03n1x
Background This question comes mainly from my ignorance. Today is Black Friday, one of my favorite days of the year to buy books. One boo...
New
crispinb
On reading dhh’s latest The One Person Framework it strikes me that Phoenix with LiveView is already pretty much this. However, never hav...
New
matthias_toepp
I’d love to hear what people think about Wisp, the new Gleam web framework started by Gleam’s primary creator Louis Pilfold. Gleam, alon...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 130579 1222
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New

We're in Beta

About us Mission Statement