Benito Serna Tips and tools for Ruby on Rails developers

Testing functions with side effects in elixir (by a functional noob)

August 25, 2015

Talking about object oriented programming, Sandy Metz propose that when you are testing “outgoing messages with side effects” what you should do is just assert that the “outgoing message” has been sent. For example in ruby we can do something like…

it "updates brand name" do
  repo = TestRepo.new
  brand = TestBrand.new(id: 1, name: "Aple")

  expect(repo).to receive(:update_attributes).with(name: "Apple")
  Brands.update_brand_name(brand, {name: "Apple"}, repo: repo)
end

I think that this is a very nice way if you are using ruby or a language where keeping state is possible (or easy).

But when you are using something like Elixir, trying to make something similar feels a little funky.

I have tried several ways to test that kind of “outgoing messages with side effects” in elixir and I want to explain them a little.

Note: Maybe in elixir we should not call this kind of operations “outgoing messages with side effects”, so I will call them just “functions with side effects”, but I am not sure if this is the right term.

Assert the side effect

test "updates brand name" do
  Brands.update_brand_name %Brand{id: 1, name: "Appl"},
   %{"name" => "Apple"}, repo: Repo,

  assert Repo.find(1).name == "Apple"
end

I think doing this kind of tests are easy, but when the things get a little more complicated or you have to call services that you have no test environment you need to take another approach.

Assert side effects on a double

defmodule FakeRepo do
  def start() do
    # Agent.start_link(...)
  end

  def update(record, attrs) do
    # Agent.update(...)
  end

  def insert(record) do
    # Agent.update(...)
  end

  def find(id) do
    # Agent.get(....)
  end
end

test "updates brand name" do
  Brands.update_brand_name %Brand{id: 1, name: "Appl"},
   %{"name" => "Apple"}, repo: FakeRepo,

  assert FakeRepo.find(1).name == "Apple"
end

I think this is an “easy to think” solution, because is just doing a simpler dummy implementation of the real Repo but can be also a dummy of a payment gateway or other external service.

I have been using this technique for some months because I had’t found a better solution. But I think that I have been doing a lot of work implementing this dummy “services” and also I think this just feels funky.

Test just the output

defmodule FakeRepo do
  def update(changeset) do
    if changeset.valid? do
      {:ok, updated_model(changeset)}
    else
      {:error, changeset}
    end
  end

  defp updated_model(changeset) do
    changeset.model
    |> Map.merge(changeset.changes)
  end
end

test "it can modify the name" do
  {:ok, brand} = Brands.update_brand_name %Brand{id: 1, name: "Appl"},
    %{"name" => "Apple"}, repo: FakeRepo

  assert brand.name == "Apple"
end

test "it returns the edit page when there is an error" do
  {:error, changeset} = Brands.update_brand_name %Brand{id: 1, name: "Pumps"},
    %{name: nil}, repo: FakeRepo

  assert {:name, "can't be blank"} in changeset.errors
end

We can take advantage that the repo always returns a tuple like {:ok, _record} when the changeset is valid and the operation is success, and a tuple like {:error, _changeset} when the changeset is invalid.

So we can test that our code does the validations that we want and also that call the repo in the expected way. An we are just using functions!, we don’t need to keep state or create a process, just functions =).

Conclusion

I think we should try to test the most of our code in this way, because is by far simpler and also I think is a better solution when you are working in a functional language.

UPDATE

I think a have discovered a much better way to test functions with side effects in elixir, thanks to Henrik Nyh and Jose Valim.

The trick here is to assert that a message has been sent to the current process. For our example we can do something like:

Test just the output

test "it updates the name in the store" do
  defmodule StoreSpy do
    def update(id, attributes) do
      send self, {:store, :update, id, attributes}
    end
  end

  brand_id = random_number
  Brands.update_brand_name %Brand{id: brand_id, name: "Appl"}, %{"name" => "Apple"}, StoreSpy
  assert_received {:store, :update, brand_id, %{"name" => "Apple"}}
end

I think this is just what I was looking for =).

Related articles

Weekly tips and tools for Ruby on Rails developers

I send an email each week, trying to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or All post by topic.