Strategies for Testing Processes

I’ve run into a few challenges so far with testing Elixir processes. I would appreciate any insight you can provide.

Scenario: Process Communication

We generally build GenServer processes in the form:

defmodule Whatever do
  # Client API

  def call_this(pid_or_name)
    GenServer.call(pid_or_name, …)
  end

  # Server API

  def handle_call(…)
    # ...
  end
end

Then let’s say I have code in another process that ends up doing something like:

defmodule Other do
  def handle_call(…)
    # …
    Whatever.call_this(pid_or_name)
  end
end

What’s the best way to test Other without also needing to work with Whatever or use mocking?

I can pass the module name to use into the process, but then I end up using apply() everywhere internally and building parallel test APIs for everything I need to stub at some point. I’m open to better ideas.

Scenario: Periodic Task Processes

Let’s say that I want a process to periodically do some task. If it’s a simple process, I could use Process.sleep/1 in the main loop or :timer.send_interval/2.

These are a pain to test, if the process manages this internally, but I’ve found that I can make a process respond to a message to trigger the task and eventually trigger that with :timer.send_interval/2 externally.

The move to GenServer makes this worse. Process.sleep/1 is out because I don’t control the loop. :timer.send_interval/2 means I need to construct the internal message format, which feels wrong.

My best idea is to add a process that periodically calls the correct interface function for me. (And I’m back to apply().) Are there better ways?

Scenario: Kick-starting Well Tested Processes

In solving some of these issues, I find myself facing a new challenge. I have well tested processes that just need to be wired up to each other, set to receive periodic messages, or whatever. Now the question becomes: where do I put that code?

In the OTP application structure, where do I put the little bit of “glue” or “matchmaking” code that just needs to run on startup?

3 Likes

What I found in my first OTP learning app was the code telling me to push identification of collaborators into start_link calls and store them in a GenServer’s state. Those in turn the supervisor can read from config data in production. But in the test, you can start_link Other and pass in self as the pid to substitute in as a callback process. Then you can assert_receive or similar.

1 Like

@JEG2 Can feel your pain.

Let me answer this from my perspective on a more general level:

(1) There is no rule that says have your handle_call in this function. Your scenario 1 calls the other server. What is the code before and after the call. What do you try to test? If it is just a call to Whatever than you need to test Whatever - perhaps having the code for handling the call in the module that can be tested easily.

(2) Mocking: It has its disadvantages, but should not be ruled out all the time. I have seen lots of abuse of mocking, but also useful case. Let’s assume that - again scenario 1 - the call to Whatever returns a critical state, then you would like to mock it to create different use cases - error, timeouts, different states. In my opinion, the call for some sort of dependency injection can lead to the same abuse than mocking.

(3) Kick-starting … Are the processes supervised? Then the code - I guess matchmaking means to pass an initial state - would be in the code that starts the children.

1 Like