Fl4m3Ph03n1x

Fl4m3Ph03n1x

Mock is crashing process in umbella project

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:

  1. A problem with the library Mock
  2. A problem with my setup of the library
  3. A problem with the test that causes the process to crash

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?

Marked As Solved

Fl4m3Ph03n1x

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

Also Liked

LostKobrakai

LostKobrakai

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. That architecture cannot support concurrent tests. The best improvement to get would be better errors or disclaimers.

What you call an issue is imo a good driver for well rounded mocking.

There’s the guideline of “don’t mock what you don’t own”. You don’t own the API of File – the core team does. Elixir does well with not doing backwards incompatible changes, but they could always add new return values and such. You might not become aware of those additional return values, so you won’t be testing for those, which might break your code in production while even well setup tests – working on an incomplete assumption of the interface – would suggest everything is fine.

Instead you want to create your own interface (in the form of a behaviour), around the actual usecases you have for interacting with the filesystem. Let’s call it MyApp.FileStorage. Then you own the interface between your code and the underlying implementation using File’s API (MyApp.FileStorage.LocalFiles), as well as the implementation you use in tests (MyApp.FileStorage.Mock).

Changes in File’s APIs then no longer affect your mocked interface MyApp.FileStorage. They only affect the implementation MyApp.FileStorage.LocalFiles, which you hopefully tested separately without mocks to ensure it works correctly. Those tests hopefully fail before you push to production. All tests using the mock implementation would be unaffected.

One sideeffect of that approach is also that your interface might becomes smaller. Instead of the whole File API you’ll likely shrink the mocked interface to a few more select things your codebase actually needs, potentially even shrinking the number of possible parameters and return values as well. Complex tasks, which require multiple calls to File API might become a single callback on your behaviour, again simplifying the interface and how much work it would be to mock.

LostKobrakai

LostKobrakai

I certainly have made my point and I’ll stop adding comments in regards to that going forward. Though I’d argue that my points will aid in having more code under control, and therefore being able to be tested as much as possible, rather than less. The gaps will be exactly the impossible to cover gaps one has with any approach. So I think we do align on the goal.

LostKobrakai

LostKobrakai

So you cannot use automated tests – that’s a real and valid limitation. But even if you cannot have that specific benefit of testing the production implementation (which you couldn’t test anyways) you still get all the benefits of having the mocked interface not mapping to that external http API.

The only thing changes in that http api can break is your implementation of your own interface with said http API. It however cannot change your interface itself and to a certain degree your mock can get away not being affected as well.

There’s a few ways your external http api can change:

  1. Endpoints change, but there are alternatives – potentially doing multiple requests – still using the same data as before, ultimately returning all the date you need.
    1. HTTP API is the interface → You need to adjust all code/tests/mocks interacting with that externally supplied interface
    2. You have your own interface → You adjust the implementation with the http API only.
  2. Endpoints change and either need more data than before or no longer return data you depended on before
    1. HTTP API is the interface → You need to adjust all code/tests/mocks interacting with that externally supplied interface
    2. You have your own interface → You need to adjust your interface by the minimal amount of changes the http api changes pushed onto your interface (might just be a subset of the http api changes)
  3. Stateful interaction between multiple API calls change
    1. HTTP API is the interface → You need to adjust all code/tests/mocks interacting with that externally supplied interface
    2. You have your own interface → If those changes are contained within a single callback of your interface, then only the implementation with the http api might need to change. If it affects interaction between multiple callbacks of the interface, then any interaction with your interface might need adjustment. You might however also be able to just adjust the implementation with the http api by introducing your own stateful handling correcting for the changes vs how your interface is used.

Actual real life changes might be a combination of those cases.

Having you own interface here is therefore an effective means of limiting the effects external changes can have onto your system. That’s essentially the idea around anti-corruption layers you might be reading about in hexagonal or onion architecture, though they usually explain that in regards to data not computation.

Where Next?

Popular in Questions Top

lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
kostonstyle
Hi all I want to have a unix time, from the current time plus 1 hour. DateTime.now + 1 hour How to get it in elixir? Thanks
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
Exadra37
Sometimes I want to check if the input into a function is not a blank string. My first approach: defmodule Example do def do_stuff(s...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30048 115
New
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
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
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
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
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
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New

We're in Beta

About us Mission Statement