Isolating a Functional Core

Learn how to isolate the logic out of a GenServer's module for testing.

The example covered in the previous two lessons is basic, but the more complex the internal logic, the harder it is to understand the tests. It becomes very tempting to start reaching into the GenServer for its state more and more.

A more straightforward solution would be to move the logic out of the GenServer’s module and into a new, purely functional module. This allows us to take advantage of the ease of testing purely functional code while leaving the GenServer tests focused on what GenServer does best: maintaining state and presenting an API.

Purely functional module

First, let’s look at the tests and code for the purely functional module, which we’ll call the RollingAverageMeasurements because it’ll now handle most of our code logic. This module is concise and clearly scoped:

Press + to interact
#file path -> example/lib/measurements.ex
#add this code at the indicated place mentioned in comments of example/lib/
#measurements.ex in the playground widget
def new(max_measurements) do
{[], max_measurements}
end
def add_element({measurements, max_measurements}, new_element)
when length(measurements) < max_measurements do
{[new_element | measurements], max_measurements}
end
def add_element({measurements, max_measurements}, new_element) do
without_oldest = Enum.drop(measurements, -1)
{[new_element | without_oldest], max_measurements}
end
def average(measurements) do
Enum.sum(measurements) / length(measurements)
end

The RollingAverageMeasurements module now contains all of the pure data ...