Background
I have an umbrella project, where I run mix test
from the root.
In one of the apps, I am mocking the File module using the Mock library.
Problem
The issue here is that when I run mix test
the process dies, with no error message to show:
Manager.Impl.Store.ReaderTest [test/unit/store/reader_test.exs]
* test list_syndicates/1 returns the list of all known syndicates [L#496]** (EXIT from #PID<0.98.0>) killed
Code
The code of the test is as follows:
defmodule Manager.Impl.Store.ReaderTest do
use ExUnit.Case, async: false
alias Manager.Impl.Store.Reader
import Mock
setup do
%{
file_io: File,
paths: [syndicates: ["syndicates.json"]]
}
end
describe "lists syndicates" do
test_with_mock "returns the list of all known syndicates", %{paths: paths} = deps, File, [],
read: fn _filename -> {:ok, "[\"utc\"]"} end do
# Act
actual = Reader.list_syndicates(deps)
expected = {:ok, [%Syndicate{name: "UTC", id: :utc, catalog: []}]}
expected_path = Path.join(paths[:syndicates])
# Assert
assert actual == expected
assert_called(File.read(expected_path))
end
end
end
In comparison, the follow test (which does not use mock) works just fine:
defmodule Manager.Impl.Store.ReaderTest do
@moduledoc false
use ExUnit.Case, async: false
alias Manager.Impl.Store.Reader
import Mock
setup do
%{
paths: [syndicates: ["syndicates.json"]]
}
end
describe "list_syndicates/1" do
defmodule FileMockListSyndicates do
@moduledoc false
def read(path) do
assert path == "syndicates.json"
{:ok, "[\"utc\"]"}
end
end
setup do
%{
file_io: Manager.Impl.Store.ReaderTest.FileMockListSyndicates
}
end
test "returns the list of all known syndicates",
%{paths: paths} = deps do
# Act
actual = FileSystem.list_syndicates(deps)
expected = {:ok, [%Syndicate{name: "UTC", id: :utc, catalog: []}]}
# Assert
assert actual == expected
end
end
end
To me this is rather surprising. One alternative crashes the process with no error message, while the other makes everything work.
To me, this indicates one of three problems:
I believe the second and third options to be the most probable, but without any information about the error, I can't be sure. The process simply dies.
Question
Why is my process dying, and how can I fix it?
Mock
depends on:meck
, which does swap out the complete module within the runtime – as in make the VM unload the existing module and load the module with the mock code. (...) The best improvement to get would be better errors or disclaimers.
Source: https://elixirforum.com/t/mock-is-crashing-process-in-umbella-project/61017/4?u=fl4m3ph03n1x
At the time of this writing, I have tested all major mocking libraries (Mock, Mox and Mimic).
Both Mock and Mimic rely on :meck
underneath. Rather surprisingly, I get the exact same issue with Mimic as I got with Mock, i.e., the process crashes without warning.
This leaves me to the only logical conclusion, which is that the problem I am facing is related to the underlying system that servers as a base for both libraries: :meck
. This issue does not happen using Mox.
I have therefore decided not to use Mock (nor Mimic) for the application and I am instead injecting the dependencies directly into the functions that need them. This last approach is very lightweight and does allow for async: true
which gives a noticeable speed increase when running mix test
.
Because I am only using a very small portion of the external system's API (2 - 3 functions) this fits well with my needs. However, If I were to use all functions from said API (let's say 20) this solution would be rather difficult to manage.
I don't expect the affected modules to evolve in such a direction, so for the time being, I am rather happy.
Here is a sample test for those searching for inspiration (using File
as an example):
setup do
%{
paths: [products: ["products.json"]]
}
end
test "returns list of available products from given syndicate", %{paths: paths} = deps do
read_fn = fn filename ->
assert filename == Path.join(paths[:products])
{:ok, "[{\"name\": \"utc\"}]"}
end
deps = Map.put(deps, :io, %{read: read_fn})
syndicate = Syndicate.new(name: "UTC", id: :utc)
assert FileSystem.list_products(syndicate, deps) ==
{:ok, [Product.new(%{"name" => "Bananas"})]}
end