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:
- Elixir 1.5
- Erlang version 20
- mock by jjh42
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:
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:
A function to accomplish this:
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:
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:
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:
Will fail:
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.