Red Green Repeat Adventures of a Spec Driven Junkie

Mocking Elixir IO.gets with Agent

I am learning Elixir and picking up the best pieces from other languages I have learned. That means I am learning Elixir using TDD, or Test Driven Development.

So far, I have learned how to get tests going with ExUnit, Mocking out command line output, IO.puts using jjh42’s mock library.

Now, I will go over how to mock elixir’s input, IO.gets, using the standard way to get consistent return, but also another way returning multiple output values with the same input value, simulating a real user.

System Setup

If one wishes to follow along, the current system setup:

  • Elixir 1.5
  • Erlang version 20

These are the latest at the time of writing.

A repository of all this is available here

To follow in a Vagrant box, install vagrant

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

Mocking IO.puts

When mocking out IO.gets, it is easy to assign single value as the return value using the mock library:

# test/mock_input_simple_test.exs
defmodule MockInputSimple do
  use ExUnit.Case, async: false

  import Mock

  test_with_mock "single output", IO, [gets: fn(_) -> "1" end] do
    assert IO.gets("") == "1"
    assert IO.gets("") == "1"
  end
end

This is straight from the mock library’s documentation.

In this case, IO.gets will return the same value every time.

Changing Value?

What about a more complicated return value? Like a value that will change with the same call to IO.gets?

In functional languages, obtaining a different output from the same input is impossible since the nature of functional programming is to have the same output for the same input.

With IO.gets(), if the input changes, the mock can compensate for that by:

# test/mock_input_simple_2_test.exs
defmodule MockInputSimple2 do
  use ExUnit.Case, async: false

  import Mock

  test_with_mock "single output", IO, [gets: fn(x) -> x + 1 end] do
    assert IO.gets(1) == 2
    assert IO.gets(2) == 3
  end
end

To achieve different output from the same input, it is not possible using this mocking method. There has to be state stored outside of the function and accessible.

Enter: Agents

In Elixir, Agents are a process that holds state. Other functions can query agents or even store values in agents.

Agents run as a background process, so they can persist state. If IO.gets can query an agent instead of a function for it’s value, a different value can result from the same input.

Replicate Same Value

First, let’s get the same functionality as the current mock: return the same value from the same input using agents.

So, this is one way have an Agent return the same value:

# lib/mock_input_simple_agent.ex
defmodule MockInputSimpleAgent do
  use Agent

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

  def get() do
   "1"
  end
end
  • start_link is a way for the agent to be started. This function is required for all agents.

  • fn -> %{} end is a function initializing a Map

  • __MODULE__ is a way to get the current name of the module. In the above code, the module name will be MockInputSimple.

  • %{} is a way to create a HashDict # TODO: check on syntax meaning that can store a value in any entry using the same key. The key “index” will store the value in these examples.

Before incorporating this with the current function, the Agent must be running. If the Agent is not running, the process won’t return anything.

To start the agent, start_supervised is the command to start the Agent.

Putting everything together into a test:

1
2
3
4
5
6
7
8
9
10
11
12
13
# test/mock_input_simple_agent_test.exs
defmodule MockInputSimpleAgentTest do
  use ExUnit.Case, async: false

  import Mock

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

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

This replicates the previous setup. This is a lot of work to achieve one line of code: fn(_) -> "1" end.

The important thing: Agent is working and returning values. Let’s add to this and have it do more, like setting and retrieving a value.

Get & Set Values

The way to have a changing value is to get the stored value and save an incremented value.

To save a value, have the Agent set value:

def set(value) do
  Agent.update(__MODULE__, &Map.put(&1, "index", value))
end
  • Agent.update updates the entry for __MODULE__
  • &Map.put creates a map data structure (elixir’s key/value store) for entry “index” and stores a value.

To get the stored value, access the value through the Agent:

def get
  Agent.get(__MODULE__, &Map.get(&1, "index")) || 0
end
  • Agent.get will retrieve the value from __MODULE__
  • &Map.get retrieves the value from the “index” key.
  • || 0 will return 0 if the previous function does not return a value

Putting this together:

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
32
33
34
35
36
37
38
# lib/mock_input_agent_get_set.ex

defmodule MockInputAgentGetSet 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
end

# test/mock_input_agent_get_set_test.exs
defmodule MockInputAgentGetSetTest do
  use ExUnit.Case, async: false

  import Mock

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

    first_value = IO.gets("")
    assert first_value == 0

    MockInputAgentGetSet.set(5)

    second_value = IO.gets("")

    assert first_value != second_value
    assert second_value == 5
  end
end

So, now IO.gets returns the set value. Desire a new IO.gets value? Set it!

IO.gets returns a different value each time with the same input, but with a set in-between.

Let’s take it to the next level and the value increase after retrieval.

Get & Increment Value

With getting and setting working, let’s enhance get so it will increment after each call.

1
2
3
4
5
def get() do
  current_value = Agent.get(__MODULE__, &Map.get(&1, "index")) || 0
  set(current_value + 1)
  current_value
end
  • line 2: get the current value stored in the Agent
  • line 3: set a new value which is one greater than the current value
  • line 4: return the current value

Incorporating this function into a module and test module:

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
32
33
34
# lib/mock_input_agent_get_and_increment.ex
defmodule MockInputAgentGetAndIncrement do
  use Agent

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

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

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

# test/mock_input_agent_get_and_increment_test.exs
defmodule MockInputAgentGetAndIncrementTest do
  use ExUnit.Case, async: false

  import Mock

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

    assert IO.gets("") == 0
    assert IO.gets("") == 1
    assert IO.gets("") == 2
    assert IO.gets("") == 3
 end
end

Now IO.gets returns a different value each time without an explicit call.

Conclusion

Different ways to use Agents to mock IO.gets to produce different values depending on the situation. From returning a static value, a set value between calls, or an incrementing value on every call.

Although the fundamental design of functional languages is to have the same output from the same input, using Agents in Elixir allows different output from the same input.

Agents achieve this as they are background processes running that stores state and allows queries.

Combining Agents and Mocking is a way to simulate command line user input.

Next time, I will talk about a more sophisticated return values