Return different values depending on number of times a function is called

Viewed 196

Background

I am working in a project that uses Mock for testing. However I need to run a specific scenario, where the output of a function depends on the number of times said function is called. I don't think Mock supports this, so I am trying to find out a way to conduct this test.

Code

In this test I have a Storage module which I want to mock (it has side effects and is a boundary). In the test, I call a function get, which the first time returns nil, then I save some data with save and then I call get again.

test_with_mock "returns OK when products list is saved into storage", Storage, [],
    [
      save: fn _table, data -> {:ok, data} end,
      get: fn
        _table, _seats_number -> {:ok, nil} # first call it returns nil
        _table, _seats_number -> {:ok, [1]} # second call should return some data
      end
    ] do
      # Arrange
      products = [
        %{"id" => 1, "gems" => 4},
        %{"id" => 3, "gems" => 4},
      ]
      products_table = :products

      # Act & Assert
      actual = Engine.save_products(products)
      expected = {:ok, :list_saved_successfully}

      assert actual == expected
      
      # First call to get returns nil because the table is empty. 
      # Then we save something into it.
      assert_called Storage.get(products_table, 4)
      assert_called Storage.save(products_table, {4, [1]})

      # Second call should return the product previously saved
      # But the mock only returns nil
      assert_called Storage.get(products_table, 4)
    end

The issue here is that since there is no counter, I don't have a way of returning a different output depending on the number of calls a function was called.

To be fair, Mock does offer a way to returns different outputs when the input is different. However, this is not the case. The input is the same, the only different is the number of invocations.

Question

How can I achieve my goal using Mock?

2 Answers

You can use the function :meck.seq to create a mock that returns the given values in sequence. This relies on the fact that Mock is just a thin layer on top of meck, which allows creating mocks like this:

  test_with_mock "returns OK when products list is saved into storage", Storage, [],
    [
      save: fn _table, data -> {:ok, data} end,
      get: [{[:_, :_], :meck.seq([
               {:ok, nil}, # first call it returns nil
               {:ok, [1]} # second call should return some data
               ])}]
    ] do

That is, instead of passing an anonymous function as the implementation of the mock, pass a list of tuples, where each tuple has an "argument spec" (in this case [:_, :_], to allow any two arguments) and a "return spec", for which :meck.seq returns a magic value that makes the mock return different values each time.

You can use an agent to keep the mutable state. Here's the sketch:

defmodule MagicTest do
  use ExUnit.Case, async: false

  import Mock

  setup do
    {:ok, pid} = Agent.start_link(fn -> 1 end, name: __MODULE__)
    {:ok, %{counter: pid}}
  end

  test_with_mock "test_name", %{counter: counter}, Magic, [],
    get: fn -> Agent.get_and_update(counter, fn state -> {state, state + 1} end) end do
    assert 1 == Magic.get()
    assert 2 == Magic.get()
  end
end


defmodule Magic do
  def get do
    42
  end
end

Your test will probably be a bit more complex, but the idea is the same.

Mock readme also briefly explains how to use ExUnit context argument.

Related