unit-testingtestingerlangelixirmox

Mocking consecutive function calls in Elixir with Mock or Mox


I'm trying to mock function multiple calls, so it returns every time specific different value. I am not that familiar with Elixir and functional concepts.

defmodule Roller do
  def roll do
      1..10
      |>Enum.random()
  end
end

Roller returns a random number every call.

defmodule VolunteerFinder do
  import Roller

  def find(list) do
    find(list, []);
  end

  defp find([] = _list, result) do
    result
  end

  defp find([head | tail] = _list, result) do
    result = [%{name: head, score: Roller.roll()} | result]
    find(tail, result)
  end
end

So assuming the list contains more than one element, the roller is called 2 times. In my test, I need to control it somehow.

I tried it with Mock. I would like to do something like this in the simplest possible way. It would be great not to have to save some state anywhere or run separate processes for every call. I know Elixir thinking might be a little different than the objective paradigm mindset I have. What's the most correct Elixir way of testing the VolunteerFinder module?

defmodule VolunteerFinderTest do
  use ExUnit.Case

  import Mock
  import Roller

  test_with_mock(
    "Find volunteer for list with one element",
    Roller,
    [roll: fn() -> 5 end]
  ) do
    assert VolunteerFinder.find(["John"]) == [%{name: "John", score: 5}]
  end


  test_with_mock(
    "Find volunteer for list with two elements",
    Roller,
    [roll: fn
       () -> 2
       () -> 5
    end]
  ) do
    assert VolunteerFinder.find(["John", "Andrew"])
    == [%{name: "Andrew", score: 5}, %{name: "John", score: 2}]
  end
end

Solution

  • I found a working solution, but I am not sure if I'm satisfied with it:

      test_with_mock(
        "Find volunteer for list with two elements",
        Roller,
        [roll: fn () -> mock_roll end]
      ) do
        send self(), {:mock_return, 1}
        send self(), {:mock_return, 5}
    
        assert VolunteerFinder.find(["Sergey", "Borys"])
        == [%{name: "Borys", score: 5}, %{name: "Sergey", score: 1}]
      end
    
      def mock_roll do
        receive do
          {:mock_return, value} ->
            value
        after
          0 ->
            raise "No mocked returns received"
        end
      end
    

    Is there any more elegant way of solving the problem?