DirtyInformation

Pair program with me!

Testing Named Agents

TLDR; When testing named agents that are started by an Elixir applicaiton inject a name for the agent to eleminate intermitent failures. Screencast

We have been tasked to work on our company’s trade secret incrementing counter. It must be running at the startup of the application and we don’t want to keep passing the name of the agent around since there should be only one.

First we have an application that starts up a named agent.

defmodule AgentTest do
  use Application

  def start(_, _) do
    import Supervisor.Spec

    children = [
      worker(AgentTest.Counter, [], restart: :permanent)
    ]

    Supervisor.start_link(children, strategy: :one_for_one, name: AgentTest.Supervisor)
  end
end

Done, we are well on our way to winning.

Time to write some tests first because we are awesome developers. Our tests utilize the counter that we started in the application so there is no need to worry about starting the link on our own. This makes for a nice clutter free test.

TDD style of red/green/refactor is being skipped here for the article, but not for the video or in real life.

#Test
defmodule AgentTest.CounterTest do
  use ExUnit.Case, async: true
  alias AgentTest.Counter

  test "initial value" do
    assert Counter.get == 0
  end

  test "increment" do
    Counter.increment
    assert Counter.get == 1
  end
end

Here we have our actual counter agent. Since we don’t want to have to worry about the name of the agent we use __MODULE__ to make the name AgentTest.Counter and not worry about passing it to get and increment every time. The reason we use __MODULE__ is so that if we refactor the name of our module all the names of our processes also change.

defmodule AgentTest.Counter do

  def start_link do
    Agent.start_link(fn -> 0 end, name: __MODULE__)
  end

  def get do
    Agent.get(__MODULE__, fn(count) -> count end)
  end

  def increment do
    Agent.update(__MODULE__, fn(count) -> count + 1 end)
  end
end

Time to run our tests.

amos@Noggin:~/workspace/$ mix test
Compiling 1 file (.ex)
...

Finished in 0.02 seconds
3 tests, 0 failures

Randomized with seed 565235

What a fantastic day! We ship the code off to the rest of the team as we saved the day with trade secret counter implementation. A few minutes later we notice our build is broke and like any good teammate we jump in to fix the problem. We open up the build log.

Running tests
.

1) test increment (Invoyer.DataAccess.ClientTest)

    Assertion with == failed
    code: Counter.get() == 0
    left: 1
    right: 0
    stacktrace:
      test/counter_test.exs:6 (test)

.

Finished in 0.02 seconds
3 tests, 1 failure

Randomized with seed 196352

What happened?!? Everything was working when we pushed. I wonder who broke the code? Hold off, cowboy, it isn’t a good idea to start pointing fingers, but we can check git praise. Hmm, it looks like the last commit was us. Let’s run the tests locally a few times. times produces a different result. This is Elixir and it can’t be a threading issue. Now what do I do?

amos@Noggin:~/workspace/$ mix test
...

Finished in 0.02 seconds
3 tests, 0 failures

Randomized with seed 565233

amos@Noggin:~/workspace/$ mix test
.

1) test increment (Invoyer.DataAccess.ClientTest)

    Assertion with == failed
    code: Counter.get() == 0
    left: 1
    right: 0
    stacktrace:
      test/counter_test.exs:6 (test)

.

Finished in 0.02 seconds
3 tests, 1 failure

Randomized with seed 296355

As we dig in we start to realize that we have one named agent that is being utilized for every test. Since we have one agent storing the state for multiple tests there is a posibility that the tests are stepping on each other. It looks like our incementation test is incrementing the value before our inital get test is running assertions. How do we fix this?

We could start up the agent we need in the test and make sure we run our specs with mix test --no-start. Then we would have to change the build and let the rest of the team know how to run the build. We might have to change multiple tests later to handle their own processes. We might be able to tag some tests and run some that start the application and others that don’t. This all seems quite complicated and has a lot of cognitive overhead for the long haul. There has to be a better way.

I guess we are just going to have to give it a name with every call, but that is going to be annoying and we don’t want to deal with that.

What if we inject a name just for our specs? We defualt the name in out application, but we allow some options to be passed into our counter that include a name. In the future we could kick off another counter with a name too, but that is YAGNI for now. Lets try it out.

First we update our specs to pass along a name at each point. Let’s use the test module as the name of our counter. That way debugging is simple. We will have to add a setup that starts a new agent and passing in the name of the agent. We don’t want to forget that our setup needs to return :ok since we don’t need any of the state to be passed along. We also need to make sure each spec is using the same agent. We can just pass in the name to our get and incement functions.

defmodule AgentTest.CounterTest do
  use ExUnit.Case, async: true
  alias AgentTest.Counter

  setup do
    Counter.start_link(name: __MODULE__)
    :ok
  end

  test "initial value" do
    assert Counter.get(__MODULE__) == 0
  end

  test "increment" do
    Counter.increment(__MODULE__)
    assert Counter.get(__MODULE__) == 1
  end
end

Running our tests now leads to many failures because we don’t have any matching functions. So, lets go ahead a fix those. We will first pull up the __MODULE__ to an attribute called @name. This makes the code look a little nicer and lets us easily change the default. Next we need to make sure that we provide the name as a default to all our functions except start_link. Since start_link will take an options map we will use Keyword.put_new/3 to good work in setting up a default name for us.

defmodule AgentTest.Counter do
  @name __MODULE__

  def start_link(opts \\ []) do
    opts = Keyword.put_new(opts, :name, @name)
    Agent.start_link(fn -> 0 end, opts)
  end

  def get(name \\ @name) do
    Agent.get(name, fn(count) -> count end)
  end

  def increment(name \\ @name) do
    Agent.update(name, fn(count) -> count + 1 end)
  end
end

With all this code in place we run our tests one last time, or fifty times because we are scared of the Heisenbug coming back.

amos@Noggin:~/workspace/$ mix test
Compiling 1 file (.ex)
...

Finished in 0.02 seconds
3 tests, 0 failures

Randomized with seed 1238712

#...

Everything seems like it is working well. We better push this up and then share our new knowledge with the rest of the team.

The actual code can be found on github and if you would like here is the screencast.

The Past

Contact Me

Company
Binary Noggin
Email
contact@binarynoggin.com
Github
Adkron
BinaryNoggin
Twitter
@adkron
@binarynoggin
Podcast
This Agile Life