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
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!
- 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?)
- 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.
- 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.
- ???
- Profit! You have fully tested loose coupled system!
Enjoy)
blatyo
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.
blatyo
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:
https://github.com/conduitframework/conduit_sqs/blob/master/test/conduit_sqs/poller_test.exs
And the module it’s testing:
https://github.com/conduitframework/conduit_sqs/blob/master/lib/conduit_sqs/poller.ex
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








