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.
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.
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.
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 =).
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.
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 "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 =).
Here I try 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 posts.