JEG2
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?
Trending in Questions
Other Trending 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
- #blog-post
- #phoenix_html
- #iex
- #graphql
- #ai
- #genstage
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex










First 2 of 2 Posts
gregvaughn
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.
kujua
@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.