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 beMockInputSimple
. -
%{}
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