devonestes

devonestes

Assertions - helpful assertions to help you write better tests

Introducing assertions, the library that helps you write really great test assertions!

GitHub: https://github.com/devonestes/assertions
Hex.pm: https://hex.pm/packages/assertions
HexDocs: https://hexdocs.pm/assertions/Assertions.html

This library aims to wrap up common types of assertions that many applications come across in a nice, reusable, composable fashion, and with really exceptional error messages.

Here’s just one example of one use case - comparing a list of structs for equality.

Imagine you have a Phoenix app, and you need to test that the response from a function is basically equal to some other list of structs that you created earlier. We can’t compare them directly because the order of the structs in the list is not guaranteed, so assert list1 == list2 won’t work. Plus, the structs in our list might have differing assocations preloaded or not, meaning we also can’t reliably do assert user1 == hd(list). So, to test this reliably, we’ll need to do something like this:

defmodule UsersTest do
  use ExUnit.Case, async: true

  describe "update_all/2" do
    test "updates the given users in the database and returns those updated users" do
      alice = Factory.insert(:user, name: "Alice")
      bob = Factory.insert(:user, name: "Bob")

      updated_names = 
        [{alice, %{name: "Alice A."}, {bob, %{name: "Bob B."}}}]
        |> Users.update_all()
        |> Enum.map(& &1.name)

      all_user_names =
        User
        |> Repo.all()
        |> Enum.map(& &1.name)

      Enum.each(["Alice A.", "Bob B."], fn name ->
        assert name in updated_names
        assert name in all_user_names
      end
    end
  end
end

But that leaves a lot to be desired. There’s extra code to pull out just the names from the response and we’re obscuring the function that we’re actually testing here. The call to Users.update_all/1 is kind of buried in there.

Let’s see how we might do this with assertions:

defmodule UsersTest do
  use ExUnit.Case, async: true

  describe "update_all/2" do
    test "updates the given users in the database and returns those updated users" do
      alice = Factory.insert(:user, name: "Alice")
      bob = Factory.insert(:user, name: "Bob")

      result = Users.update_all([{alice, %{name: "Alice A."}, {bob, %{name: "Bob B."}}}

      result
        |> Enum.map(& &1.name)
        |> assert_lists_equal(["Alice A.", "Bob B."])

      assert_lists_equal(result, Users.list_all(), &structs_equal?(&1, &2, [:name]))
    end
  end
end

It’s shorter, easier to change, and much more clear about the function being tested in this test. It also gives us really wonderful error messages, which you can see more examples of in the README on GitHub.

There are many more assertions there, and if there are any other common ones that folks uses often in their projects I’d be happy to add them. I want this to be the one library that has all the common helper functions that anyone needs to write really great tests!

Most Liked

devonestes

devonestes

The improvements in 1.8 are very helpful, but it doesn’t do the shrinking that I do in assertions, and it also doesn’t expand all variables in the argument to assert. For example, a common way to test unordered list equality is something like this:

list = [1, 2, 3]
assert Enum.all?([2, 1, 4], & &1 in list)

That gives us this failure:

  1) test example (AssertionsTest)
     test/assertions_test.exs:8
     Expected truthy, got false
     code: assert Enum.all?([2, 1, 4], &(&1 in list))
     arguments:

         # 1
         [2, 1, 4]

         # 2
         #Function<6.120201396/1 in AssertionsTest."test example"/1>

     stacktrace:
       test/assertions_test.exs:10: (test)

So we can see the one list, but we can’t see the elements in the other list that we’re checking for unordered equality. And then there’s also the problem of that not actually checking equality, which many people think it does :wink:

But if we use assert_lists_equal then we write the test like this:

list = [1, 2, 3]
assert_lists_equal(list, [2, 1, 4])

Which gives us this output:

  1) test example (AssertionsTest)
     test/assertions_test.exs:8
     Comparison of each element failed!
     code:  assert_lists_equal(list, [2, 1, 4])
     arguments:

         # 1
         [1, 2, 3]

         # 2
         [2, 1, 4]

     left:  [3]
     right: [4]
     stacktrace:
       test/assertions_test.exs:10: (test)

So there we can see the differing elements between the two lists in left: and right: , which I find really helpful. Since the standard diffing that we do with something like list1 == list2 assumes that order matters, it doesn’t work when comparing lists where the order doesn’t matter (which is a great deal of the time). I thought the best way to show the diff would be to remove the common elements.

I also do this shrinking when comparing structs/maps with assert_maps_equal/3 - it will just show you what’s different instead of showing you the whole big map, and you still get the colored diff if the maps are nested.

josevalim

josevalim

Creator of Elixir

Beautiful.

devonestes

devonestes

For the pin thing, I get around that by doing:

result = some_function(data)
expected = %{id: data.id, name: orginal.name}
assert_maps_equal(result, expected, Map.keys(expected))

Also, one thing to note is that the function you pass to assert_lists_equal should return true or false, not raise an exception (which is what assert does under the hood), so you’d actually want:

import Assertions

assert_lists_equal(left, right, &ids_match?/2)

def ids_match?(%{id: id}, %{id: id}), do: true
def ids_match?(_, _), do: false

This is because the “shrinking” that happens to give the really great error messages relies on being able to compare every element in one list to every element in the other, and if you’re asserting then it’ll fail if there is any element in one list that isn’t equal to any other element in the other.

However, what I do is have functions like structs_equal? or maps_equal? that take a set of keys to use to determine if the two elements are equal in this scenario, like this:

import Assertions

assert_lists_equal(left, right, &structs_equal?(&1, &2, [:id]))

def structs_equal?(left, right, keys) do
  Enum.all(keys, fn key -> Map.get(left, key) == Map.get(right, key) end)
end

But that’s totally the intention behind all these assertions - to give you the basics that then allows you to use your own comparison functions to determine equality for the situation you’re in. == just doesn’t always cut it, ya know :wink:

josevalim

josevalim

Creator of Elixir

I wonder if you should use assert lists_equal(...) instead? From Elixir v1.7 or v1.8 we break the arguments apart in the output so there is a chance the default output is quite good without creating new assertions but only with new auxiliary functions. How do the error outputs compare?

Ninigi

Ninigi

That looks pretty neat. Can you link to where these are documented? I googled, but I couldn’t find anything.

EDIT: Oh wait, I guess I misinterpreted this… You mean you could write a function to do the equal and then use inside the assert, not that there are predefined functions for this in ExUnit or somewhere, right? :smile:

Where Next?

Popular in Libraries Top

Qqwy
TypeCheck: Fast and flexible runtime type-checking for your Elixir projects. Core ideas Type- and function specifications are const...
336 13801 100
New
hpopp
After just over two years in development, this latest version of Pigeon is what I finally consider done in regards to my original vision ...
New
sabiwara
Dune is a sandbox for Elixir and aims to safely evaluate user-provided code. You can try it out using this basic Elixir playground made ...
New
arkgil
Hi all! I’m happy to announce that Telemetry v0.3.0 is out! This release marks the conversion from Elixir to Erlang so that all the libr...
New
benlime
LiveMotion enables high performance animations declared on the server and run on the client. As a follow up to my previous thread A libr...
New
Qqwy
Hello everyone, I wrote a small library today called MapDiff. It returns a map listing the (smallest amount of) changes to get from map...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
Crowdhailer
Experimenting with this code. OK.try do user &lt;- fetch_user(1) cart &lt;- fetch_cart(1) order = checkout(cart, user) save_or...
New
OvermindDL1
I created a new library (rather I pulled out a couple files from my big project), it manages an operating system PID file for the BEAM. ...
New
wojtekmach
Hey everyone! Req is an HTTP client for Elixir that I’ve been working on for quite some time. There is already a lot of HTTP clients out...
New

Other popular topics Top

peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New

Sub Categories:

We're in Beta

About us Mission Statement