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:
$ 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.