Fl4m3Ph03n1x

Fl4m3Ph03n1x

How to test a GenServer?

Background

Let’s assume I have a typical GenServer that receives messages as requests, does some operation in a DB and returns responses.

I want to know what are the best practices for testing GenServers (or Agents) in Elixir (if there are any) and which tools people use to do so.

My approach

If we follow the accidental Actor’s model Elixir and Erlang have implemented, then testing would be straight forward - simply make sure that your Actor sends the correct amount of messages to it’s collaborators once it gets a request.

Problems with my approach

Even ignoring the fact that the actors model is quite accidental in erlang/elixir and therefore lacks some of the tools to inspect communications (as in you’d have to create them yourself) this approach has one big problem: it would require me to follow the actors model.

This may seem contradictory, but if we follow such a model strictly we will end up using processes to save and manage state, instead of using them because of the run-time benefits they can offer. This, is an issue.

However, I don’t know of a better idea to test GenServer. How do you do it?

Most Liked

Virviil

Virviil

From the general testing approach, testing callbacks seems to be incredibly bad idea.

When writing unit test, one should understand what is unit. From the point of OTP view, single GenServer is single unbreakable unit - a kind of black-box, which is receiving and sending messages, is holding state and is doing the job.

By the way, it’s the same description as for a class instance in OOP language.

Have you every heard, that instance methods are tested without instance? No, of course. So, why do you think you can break entire GenServer implementation? How can you call this kind of testings - sub-unit, half-unit, under-unit?

Now: when you see that you can’t test GenServer as a black-box -what does that mean?
It means that your implementation is very tight coupled.

While in OOP world, you think a lot about dependency injection, composition, and all this kind of stuff - why you forget about them here? Because it’s functional language?

Now, as you see, your question is not about how to test GebServer but about how to create TESTABLE GenServer. The answer is simple, because OOP guys already know the answer!

  1. GenServer callbacks IS wrapper code, adapter for OTP stuff. This adapter should adapt another module, who is implementing real business logic and does know nothing about OTP. This module should know nothing about he’s called via messages, callback or anything like this. Think about it as about pure functional module. For sure, you know how to test it?)
  2. But we still need to test our GenServer communications. So, what to do? We should replace our tight copling with loose coupling. By dependency injection of course! Do you remember pure functional module on the previous step? It has interface that in Elixir world is called behaviour. Make your GenServer call not a specific module, but any module that implements this behaviour.
  3. Mock all heavy calculations using new defined behaviour. You already know, that they are working - they are tested inside your pure functional module. You just need to test cross-process communication now. Use Mox library from Plataformtec for these needs.
  4. ???
  5. Profit! You have fully tested loose coupled system!

Enjoy)

18
Post #5
blatyo

blatyo

Conduit Core Team

To be clear, I think the things you’ve suggested are useful techniques. I think the way you’ve written it implies that it is the only “right” way and that it is comprehensive in solving all problems related to testing GenServer’s. That might be a mischaracterization of your intent and if so, I apologize.

I’ll disagree with this for two very specific reasons.

The first is, some processes are never called by any other process. Instead they call themselves periodically based on their state. For those, they generally only have handle_info calls. I think it is perfectly acceptable to call them directly.

The second is, when you have many processes communicating with a process it introduces non-determinism. That non-determinism, makes it incredibly hard to test particular types of situations. For example, imagine I have 3 processes, A, B, and C. A and B, both send messages to C. If the messaging cadence is A, A, B, B, everything works fine, but when it’s A, B, A, B, things will break. How should I test this scenario. If I’m depending on sending messages with actual processes, I have no guarantee that the messages will be sent in the desired order. However, if I call the callbacks directly myself, I do:

{:ok, state} = C.init(opts)
{:noreply, state} = C.handle_info({:blah, A, 1}, state)
# assertion about state
{:noreply, state} = C.handle_info({:blah, B, 1}, state)
# assertion about state
{:noreply, state} = C.handle_info({:blah, A, 2}, state)
# assertion about state
{:noreply, state} = C.handle_info({:blah, B, 2}, state)
# assertion about state

You seem to be arguing here for something like the following:

defmodule MyGenServer do
  @pure MyPureFunctions # pretend this gets injected with something like mox

  def init(opts) do
    {:ok, @pure.init(opts)}
  end

  def handle_call(:blah, from, state) do
    case @pure.blah(state) do
      {:now, response, state} ->
        {:reply, response, state}
      {:later, state} ->
        Process.send_after(self(), {:blah_for_real, from}, 200)
        {:noreply, state}
    end
  end
end

I wrote handle_call in a specific way to illustrate some points. One is, if you keep @pure pure, you necessarily keep some logic in the GenServer itself that needs to be tested. You didn’t necessarily say this, but I feel it’s implied that, if you pull out pure state calculations it means your GenServer is dumb. That would clearly not be the case in every situation.

Second, @pure.blah/1 in my example is aware that it needs to split its answer into two parts. The response and the state. This implies some knowledge of the calling context. On the other hand, it could return a single value that contains the state and the response. That then moves some logic into the GenServer, which adds something to be tested in the GenServer itself. The now and later results are also allowing the pure part to decide how the GenServer should respond. Perhaps the GenServer could instead infer that from the state or response, but that also puts some logic back into the GenServer. In any case, I feel it’s optimistic to assume you can always avoid knowledge of the calling context.

Third, GenServer’s often do things that are impure, like sending messages to other processes. If your state or response is derived from calling another process, you could presumably mock that process. However, in order to simulate the statefulness of that process you’re mocking out, you’d necessarily need another process.

I personally feel that the testing strategy most people use for their processes is overly simplistic. They’ll send one message to the process or only have one process communicating with their process. There are certain types of bugs that only occur after a chain of messages have happened or when multiple processes are communicating with the same process. The only way I have found to deterministically test these situation is by calling the callbacks directly and making assertions about their result and side effects. I’m woefully unaware of property testing, specifically stateful property testing, which I imagine would allow you to discover these situations exist, though, not necessarily deterministically reproduce them.

I do also want to make clear, I don’t think calling callbacks is the only way to test a GenServer. However, when I do use messages, they tend to be integration tests or the GenServer is simple enough that I’m sure determinism won’t be a problem.

15
Post #6
blatyo

blatyo

Conduit Core Team

I’m not sure if I fully understand your question. But here’s my attempt anyways.

When I’m testing GenServers, I just call the callbacks myself. So there’s generally no process involved other than the testing process.

Here’s an example of that style of test:

And the module it’s testing:

Qqwy

Qqwy

TypeCheck Core Team

Elixir and ExUnit have quite a number of tools available to them to inspect communications. It is very common to test GenServers by performing a call or cast (or a logical sequence of these) to them, and check the message inbox of the testing process for the results using for instance ExUnit’s assert_receive.

So:

  • Test the stateless parts of a GenServer by calling the callback-functions directly.
  • Test the stateful interaction with a GenServer by starting it and writing a slightly higher-level test that performs one or a couple of calls/casts to this GenServer before checking the results.
jola

jola

I agree with @blatyo, test the callbacks or module code directly.

However, if you need to control the GenServers in your tests, eg if they keep state and you want to reset it between tests, there’s start_supervised!, which handles starting and closing the processes between tests.

Where Next?

Popular in Questions Top

freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
pgiesin
This should be a simple problem but I just can’t seem to figure it out. I have a standalone Elixir app that won’t find the database. Dep...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
Mooodi
Given a string, how can I get access to its character by index? Enum.at("my_string", 2) doesn't work. Or rather, not char, but a substr...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New

Other popular topics Top

JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement