Red Green Repeat Adventures of a Spec Driven Junkie

More Mocking Elixir IO.gets with Agents

I explored how to implement a mock in Elixir where the same function, with the same input, returns a different value. One set in the test, or an auto incrementing value.

This time, I will continue exploring how to mock a feature from RSpec in Elixir, using IO.gets as the main example. The feature is:

  • return individual values from a list
  • return values in the set order
  • continue to return the last value after all others

This is the behavior in RSpec Mocks and_return method:

1
2
3
4
5
6
7
8
9
10
11
12
RSpec.describe "When the method is called multiple times" do
  it "returns the specified values in order, then keeps returning the last value" do
    dbl = double
    allow(dbl).to receive(:foo).and_return(1, 2, 3)

    expect(dbl.foo).to eq(1)
    expect(dbl.foo).to eq(2)
    expect(dbl.foo).to eq(3)
    expect(dbl.foo).to eq(3)
    expect(dbl.foo).to eq(3)
  end
end

Target Audience

This article is for those who:

  • want different mocked responses from Elixir’s IO.gets in their tests (or any other function)
  • miss RSpec’s sweet and_return(x, y, z)
  • see how Agents work

System Requirements

I developed this code in this article using:

Additionally, the code in this article and a vagrant setup file is available at: https://github.com/a-leung/mock_input

To follow along, install: vagrant.

Then run the following commands in a terminal:

$ git clone https://github.com/a-leung/mock_input.git
$ cd mock_input
$ vagrant up
$ vagrant ssh
$ cd /vagrant/
$ mix test

All the code and tests for the article work are in the repository. One can edit the code and run the tests from inside vagrant using the above command: mix test

Code So Far

To achieve different output with the same in a functional language like Elixir, using Agents to store and retrieve values for the mock.

The modules from before

MockInput Functionality
MockInputAgentGetSet Get a Set value
MockInputAgentGetAndIncrement Get and Increment a Set value

This article will build up functions from these modules

Set & Get List

Set a List

In Elixir, let’s take advantage of map’s ability to store any thing… any type of object:

["hi", "hello", "how are you?", 1, 2]

A function to accomplish this:

def set_list(value) do
  Agent.update(__MODULE__, &Map.put(&1, "list", value))
end

Get List Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
defmodule MockInputAgentGetSetListTest do
  use ExUnit.Case, async: false

  import Mock

  test_with_mock "single list output", IO, [gets: fn(_) -> MockInputAgentGetSetList.get_list() end] do
    start_supervised MockInputAgentGetSetList

    list = [1, "hello", 2, "world"]

    MockInputAgentGetSetList.set_list(list)

    assert IO.gets("") == list
  end
end

Get List Item

Modify get() function to retrieving the whole list:

def get_list() do
  Agent.get(__MODULE__, &Map.get(&1, "list")) || [nil]
end

The main difference is the index is now "list" instead of "index".

To retrieve only one item from the list, use: Enum.at on the list. get_list_item() demonstrates this functionality:

def get_list_item(index) do
  list = Agent.get(__MODULE__, &Map.get(&1, "list")) || [nil]
  Enum.at(list, index)
end

get_list_item retrieves the whole list, then Enum.at selects the item at the index.

Automatically Get List Items

Now that get_list_item(index) can retrieve any single item, how about retrieving successive items without specifying it each time?

By combining if get_list_item with MockInputAgentGetAndIncrement module’s get_and_increment for the index, the MockInputAgentAutoGetList module can become:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
defmodule MockInputAgentAutoGetList do
  use Agent

  def start_link(_opts) do
    Agent.start_link(fn -> %{} end, name: __MODULE__)
  end

  def get() do
    Agent.get(__MODULE__, &Map.get(&1, "index")) || 0
  end

  def set(value) do
    Agent.update(__MODULE__, &Map.put(&1, "index", value))
  end

  def get_and_increment() do
    current_value = get()
    set(current_value + 1)
    current_value
  end

  def auto_get_list() do
    list = get(key)
    current_index = get_and_increment()
    Enum.at(list, current_index)
  end

  def set_list(list) do
    Agent.update(__MODULE__, &Map.put(&1, "list", list))
  end
end

A simple test for this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
defmodule MockInputAgentAutoGetListTest do
  use ExUnit.Case, async: false

  import Mock

  test_with_mock "get list item at 0", IO, [gets: fn(_) -> MockInputAgentAutoGetList.auto_get_list() end] do
    start_supervised MockInputAgentAutoGetList

    list = [1, "hello", 2, "world"]

    MockInputAgentAutoGetList.set_list(list)

    assert IO.gets("") == 1
    assert IO.gets("") == "hello"
    assert IO.gets("") == 2
    assert IO.gets("") == "world"
  end
end

Safely Automatically Get List Items

Calling auto_get_list more than the specified list elements will return nil instead of the last element, so extending the previous test:

    assert IO.gets("") == 1
    assert IO.gets("") == "hello"
    assert IO.gets("") == 2
    assert IO.gets("") == "world"
    assert IO.gets("") == "world"
    assert IO.gets("") == "world"

Will fail:

  1) test get list item at 0 (MockInputAgentAutoGetListTest)
     test/mock_input_agent_auto_get_list_test.exs:6
     Assertion with == failed
     code:  assert IO.gets("") == "world"
     left:  nil
     right: "world"
     stacktrace:
       test/mock_input_agent_auto_get_list_test.exs:17: (test)

.

Finished in 0.8 seconds
9 tests, 1 failure
vagrant@vagrant:/vagrant$

The behavior in RSpec: after returning the last item, every call to the mocked item will return the last item.

Let’s make this behavior with the code so far.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
defmodule MockInputSafelyAutoGetList do
  use Agent

  # reuse start_link, get, set, get_and_increment, and set_list

  def safely_auto_get_list() do
    list = get(key)
    current_index = get_and_increment()
    case (current_index > length(list) - 1) do
      true  -> List.last(list)
      false -> Enum.at(list, current_index)
    end
  end
end

So, if the value returned by current_index is greater than the last element of the list minus one, as list items are zero-indexed, just return the last item of the list instead of looking up the list item.

A simple test for this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
defmodule MockInputAgentSafelyAutoGetListTest do
  use ExUnit.Case, async: false

  import Mock

  test_with_mock "get list item at 0", IO, [gets: fn(_) -> MockInputAgentSafelyAutoGetList.auto_get_list() end] do
    start_supervised MockInputAgentSafelyAutoGetList

    list = [1, "hello", 2, "world"]

    MockInputAgentSafelyAutoGetList.set_list(list)

    assert IO.gets("") == 1
    assert IO.gets("") == "hello"
    assert IO.gets("") == 2
    assert IO.gets("") == "world"
    assert IO.gets("") == "world"
    assert IO.gets("") == "world"
  end
end

Conclusion

RSpec’s and_return(x, y, z) behavior is in Elixir by combining Elixir’s Agent and Map’s ability to store and retrieve a list allows an ordered arbitrary value.

get_list(item_index) retrieves any item at the list index or auto_get_list() retrieves successive items in the set order.

get_and_increment provides a nice way to handle an incrementing index and limiting the index to the length of the list will return the last item once past.

I missed RSpec’s sweet mocking style so much that I basically wrote and presented a similar system in Elixir using Agents. My original motivation was so I could mock Elixir IO.gets to return different values. :-)

Necessity is the mother of invention.