When testing code that uses RpcLoadBalancer, you typically don't have a multi-node cluster available. The CallDirect selection algorithm solves this by executing calls locally via apply/3 instead of going through :erpc.
Configure your load balancer for tests
Pass SelectionAlgorithm.CallDirect as the selection algorithm when starting a load balancer in your test setup:
alias RpcLoadBalancer.LoadBalancer.SelectionAlgorithm
{:ok, _pid} =
RpcLoadBalancer.start_link(
name: :my_balancer,
selection_algorithm: SelectionAlgorithm.CallDirect
)With CallDirect active:
call/5withload_balancer: :nameexecutesapply(module, fun, args)and returns{:ok, result}cast/5withload_balancer: :nameexecutesspawn(module, fun, args)and returns:ok- No
:erpccalls are made - No cluster nodes are required
Use it in ExUnit setup
A typical test module that depends on a load balancer:
defmodule MyApp.WorkerTest do
use ExUnit.Case, async: true
alias RpcLoadBalancer.LoadBalancer.SelectionAlgorithm
setup do
lb_name = :"test_lb_#{System.unique_integer([:positive])}"
{:ok, _pid} =
RpcLoadBalancer.start_link(
name: lb_name,
selection_algorithm: SelectionAlgorithm.CallDirect
)
Process.sleep(50)
%{lb_name: lb_name}
end
test "call executes the function", %{lb_name: lb_name} do
assert {:ok, 42} ===
RpcLoadBalancer.call(node(), Kernel, :+, [40, 2], load_balancer: lb_name)
end
test "cast fires asynchronously", %{lb_name: lb_name} do
test_pid = self()
assert :ok ===
RpcLoadBalancer.cast(
node(),
Kernel,
:apply,
[fn -> send(test_pid, :done) end, []],
load_balancer: lb_name
)
assert_receive :done, 1000
end
endApplication-level configuration
If your application starts a load balancer in its supervision tree, you can switch the algorithm based on the compile-time environment:
defmodule MyApp.Application do
use Application
@selection_algorithm if Mix.env() === :test,
do: RpcLoadBalancer.LoadBalancer.SelectionAlgorithm.CallDirect,
else: RpcLoadBalancer.LoadBalancer.SelectionAlgorithm.RoundRobin
@impl true
def start(_type, _args) do
children = [
{RpcLoadBalancer,
name: :my_balancer,
selection_algorithm: @selection_algorithm}
]
Supervisor.start_link(children, strategy: :one_for_one)
end
endThis uses a module attribute evaluated at compile time, which avoids calling Mix.env() at runtime (where it doesn't exist in releases).
Why CallDirect should always be used in tests
- No cluster required — tests run on a single node, so
:erpccalls to remote nodes will fail with{:error, %ErrorMessage{code: :service_unavailable}} - Deterministic —
apply/3runs synchronously in the calling process, making assertions straightforward - Fast — skips
:erpcserialization and the:pgmember lookup entirely - Isolated — each test can start its own load balancer with a unique name without interfering with other tests