tfwright

tfwright

ExUnit and the "One assertion per test" pattern

I wanted to share a pattern I learned about way back early in my Ruby/Rails dev career and has remained central to my approach to testing just about every type of application I’ve worked on since. Like everyone, I’ve read encountered a lot of “X best practice” and “Y considered harmful” arguments over the years (I actually think it might be worthwhile creating a form section to collect and discuss those here), and this is just about the only one that I agree with almost without qualification or caveat: One Assertion Per Test. The pattern itself is a test specific application of the more general principle of DAMP vs DRY approaches to code organization, also from Jay Fields (AFAIK).

In the Rails world people seemed to follow this pattern (often unintentionally) thanks to the “one-liner” syntax defacto standard test framework, rspec. But every Elixir project’s test suite that I’ve examined seems to “group” related assertion in single blocks, and worse, shares a ton of data setup in describe blocks in I think, a misguided attempt to keep the test suite DRY, rather than DAMP (although as the name implies there is quite a bit of room for compromise here).

So I thought I’d share the original post and try to sway some people :slight_smile:

So, anybody out there doing a version of this? Are people familiar with this idea and have counter-arguments to share? Tell me what you think!

Most Liked

wojtekmach

wojtekmach

Hex Core Team

I’m personally pretty wary of such rules. I think certain types of tests would be worse off by following such rule. For example, in a controller test we might want to test different aspects of a response, what’s the status, what are the headers, what’s the body, etc. To some extent we could write them using one assertion to follow the rule but I think it would be way less readable. In high-level integration tests, especially when hitting the UI, it’d be even less practical to have just one assertion per test. For very low-level tests it seems like a pretty good rule of thumb though.

al2o3cr

al2o3cr

I’m not a huge fan of strictly following this approach - (or the Rspec one-liner syntax, TBH). Taken to extremes, it can mean slowing down a test suite by a factor of 2-3x or more due to repeated setup.

For concreteness, let’s look at some “off-the-shelf” tests as created by phx.gen.auth:

It’s not complicated to rewrite this to one-per-test:

test "logs the <%= schema.singular %> out and redirects to /", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
  conn = conn |> log_in_<%= schema.singular %>(<%= schema.singular %>) |> delete(Routes.<%= schema.route_helper %>_session_path(conn, :delete))
  assert redirected_to(conn) == "/"
end

test "logs the <%= schema.singular %> out and removes the token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
  conn = conn |> log_in_<%= schema.singular %>(<%= schema.singular %>) |> delete(Routes.<%= schema.route_helper %>_session_path(conn, :delete))
  refute get_session(conn, :<%= schema.singular %>_token)
end

test "logs the <%= schema.singular %> out and shows a flash message", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
  conn = conn |> log_in_<%= schema.singular %>(<%= schema.singular %>) |> delete(Routes.<%= schema.route_helper %>_session_path(conn, :delete))
  assert get_flash(conn, :info) =~ "Logged out successfully"
end

This makes the test in question take 3x as long to run (because nearly all the work is done 3x) and IMO makes naming the blocks harder.

An approach I’ve seen to that naming problem in Rspec-land is nested describe blocks - which comes with its own “which setup will run” guessing-game, so much so that ExUnit has explicitly made describe non-nestable.

There’s also a philosophical question: what is “one assertion”? For instance, this looks like one assertion:

assert redirected_to(conn) == "/"

but redirected_to can raise, causing the test to fail, if things are not as expected:

  • if the conn is in the wrong state
  • if the status is not 302
  • if the location header is not set

So there are FOUR ways that “single assertion” can cause the test to fail. :thinking:


One place I can see this technique being useful is when practicing “strong TDD” - the strict “no code can be written without a failing test” approach. Adding “One Assertion Per Test” to that practice means there’s never a point where the coder has to decide “add an assertion to an existing test or add a new test?”

A strict rule there would be useful to overcome analysis paralysis and start typing code.

BUT

That practice also includes a “refactor” step, and IMO the tests are fair game for refactoring. If strong TDD produced the three split-out SessionController tests above, a good refactor would be to note they have identical setup and actions.

gregvaughn

gregvaughn

I used to do that in Java-land. The idea was a 1:1 mapping of reasons to fail and tests that could fail. In the Java ecosystem, in those days (pre-Rails), true unit tests didn’t even go out of process to hit the db, so there was less of a performance concern with breaking down tests that atomically.

I don’t do that any more because it’s not worth that much “design” in test suites as well as the performance cost. I now look to test suites mostly for their regression value, so that when I’m adding some new feature or refactoring something that there isn’t some surprise dependency I wasn’t aware of that I just broke.

Where Next?

Popular in Discussions Top

PragTob
Hello everyone, I know we had quite some threads (read through lots of them) about background job processing but it remains a hotly deba...
New
axelson
Decided against including more info in the title, but the gist is that Plataformatec sponsored projects will continue with the assets bei...
New
sashaafm
Piggy backing a bit on @dvcrn topic BEAM optimization for functions with static return type?, I’ve been trying to understand in a deeper ...
New
CharlesO
Erlang :list.nth simple, but 1 - based nth(1, [H|_]) -&gt; H; nth(N, [_|T]) when N &gt; 1 -&gt; nth(N - 1, T). Elixir Enum.at … coo...
New
AstonJ
Please see the new poll here: Which code editor or IDE do you use? (Poll) (2022 Edition) It’s been a while since we first asked this, I...
208 31142 143
New
rower687
Hi all, I’ve been reading a lot about the “let it crash” term and how supervising processes and the whole messaging passing make an elixi...
New
jer
I’ve been using umbrellas for a while, and generally started off (on greenfield projects at least) by isolating subapps based on clearly ...
New
shishini
I think this twitter post and youtube video didn’t get as much attention as I hoped I am still new to Elixir, so can’t really judge ...
New
und0ck3d
Hello everyone! A few days ago I’ve created a topic here about how people were creating CMSs with Elixir and Phoenix. I’ve been studying...
New
AstonJ
If so I (and hopefully others!) might have some tips for you :slight_smile: But first, please say which area you’re finding most challen...
New

Other popular topics Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement