Help To Test Your Library
· 3 min read
I like to keep applications as tiny as possible. Everything that is not related to business logic, might and should be extracted into packages. Packages are great, reusable and better testable; once created and tested (and hopefully benchmarked,) it does not require the application to bother about its internals. It just worksโข. hex.pm provides private package functionality for those hesitating to open source their general-purpose libraries.
Despite the proven robustness of external libraries, we would probably still want to test their integration though. Any library might be great on its own, but how would it fit our application quirks?
Well, greatest libraries are welcoming for integration testing. Ecto provides a sandbox for testing . Broadway comes with a DummyProducer .
Providing a sandbox might be cumbersome and tricky in general, while
DummyWorker
is a must in any library shipped with love and care. Apparently, itโs both extremely useful for users of your library and easy to implement. Follow
Broadway
โs path, send a message to the calling process instead of performing real job. That simple.
Imagine we have a library that posts a slack message into some channel within your company workspace. Our application has some actions which should end up with a message in this slack channel. The integration test should then test that a call to
perform_this_action
has some side effects actually performing a real job
and
notifies the slack channel afterward. We use homebrewed
SlackPoster
library, which is thoroughly tested and is proven to work well with posting messages. But how would we go about integration test? Check the slack channel ensuring the message had indeed arrived? Nah.
This imaginary
SlackPoster
library is probably already having a behaviour defining the callback similar to that
@callback post(message :: map()) :: :ok | {:error, reason} when reason: any()
If the library author was kind enough, they should have provided a real implementation
defmodule SlackPoster do
@behaviout Poster
@impl Poster
def post(%{} = message),
do: message |> Jason.encode!() |> Engine.send_message()
โฆ
alongside with a dummy implementation for better testing
defmodule DummyPoster do
@behaviout Poster
@impl Poster
def post(%{} = message) do
{callback_pid, message} =
Map.pop(message, :callback, @default_value)
send(callback_pid, {:slack_sent, message})
end
And now one can test the integration easily with
ExUnit.Assertions.assert_receive/3
test "slack integration" do
# wait how would I supply a pid of process here?!
assert_receive {:slack_sent, ^expected_message}
end
Yes, there is still a glitch. When itโll come to
DummyPoster.post/1
, it would require a
pid
to provide callback, and there is no way to supply it transparently without bringing test logic into application core, which is meh. Is it a deadend?โNot at all.
Your library should provide a
message proxy
process. It is to be started within a test suite (e. g. from
setup/1
callback,) to route messages to the test process. Then we might do the following.
defmodule DummyPoster do
@behaviout Poster
@impl Poster
def post(%{} = message) do
{callback_pid, message} =
Map.pop(message, :callback, Application.get_env(:my_app, :dummy_proxy))
Process.send(callback_pid, {:slack_sent, message}, [])
end
and in our test we would need to register ourselves right before
assert_receive/1
test "slack integration" do
expected_message = ...
proxy = Application.get_env(:my_app, :dummy_proxy)
proxy.register(expected_message, self())
assert_receive {:slack_sent, ^expected_message}
proxy.unregister(expected_message, self())
end
The draft implementation of proxy process is trivial.
Another way would be to use
PubSub
mechanism for routing messages, but this is slightly more cumbersome.
Enviฬo
library aims to cover a boilerplate for pubsub internals, allowing simple drop-in implementations for publishers and subscribers. One day Iโd write about it in details too.
Happy making your customers happy testing!