Search code examples
testingelixirex-unit

How to implement an "assert_difference" for ExUnit


I'd like to test how a function changes something in the database. I'm struggling with an ExUnit equivalent of the following ActiveSupport::TestCase test case:

test "creates a database record" do
  post = Post.create title: "See the difference"
  assert_difference "Post.published.count" do
    post.publish!
  end
end

The RSpec version is more elegant and, because of its use of lambdas, something I thought was portable to Elixir/ExUnit.

it "create a database record" do
  post = Post.create title: "See the difference"
  expect { post.publish! }.to change { Post.count }.by 1
end

Is there a more elegant (read: functional) way to do it than this:

test "creates a database record", %{conn: conn} do
  records_before = count_records
  post(conn, "/articles")
  records_after  = count_records

  assert records_before == (records_after - 1)
end

defp count_records do
  MyApp.Repo.one((from a in MyApp.Article, select: count("*"))
end

Solution

  • You can use macros to get something close to the TestUnit and RSpec examples from Ruby:

    defmacro assert_difference(expr, do: block) do
      quote do
        before = unquote(expr)
        unquote(block)
        after_ = unquote(expr)
        assert before != after_
      end
    end
    
    defmacro assert_difference(expr, [with: with], do: block) do
      quote do
        before = unquote(expr)
        unquote(block)
        after_ = unquote(expr)
        assert unquote(with).(before) == after_
      end
    end
    
    test "the truth" do
      {:ok, agent} = Agent.start_link(fn -> 0 end)
    
      assert_difference Agent.get(agent, &(&1)) do
        Agent.update(agent, &(&1 + 1))
      end
    
      {:ok, agent} = Agent.start_link(fn -> 0 end)
    
      assert_difference Agent.get(agent, &(&1)), with: &(&1 + 2) do
        Agent.update(agent, &(&1 + 2))
      end
    end
    

    But I wouldn't use it unless it's going to be used a lot or else this would only make the code harder to follow for everyone (possibly) except the author. If you do use it though, you might want to move it to a different module and import that in your test modules.