Search
Search docs, blog posts, and ecosystem packages with citations.
Enter a query to see grounded citations.
This is the staging site. Canonical host: stage.jido.run
We can't find the internet
Attempting to reconnect
Search docs, blog posts, and ecosystem packages with citations.
Unit and integration test patterns for agents, actions, and runtime workflows.
Mix.install([
{:jido, "~> 2.1"},
{:jido_ai, "~> 2.0"}
])
Logger.configure(level: :warning)
# Livebook imports can execute generated docs as doctests.
# Disable compiler docs until the current Jido Hex release drops the invalid signal_types/0 example.
Code.put_compiler_option(:docs, false)
import ExUnit.Assertions
Jido.start()
runtime = Jido.default_instance()
Agents are immutable structs. Most tests need no processes, no mocks, and no async coordination. Call cmd/2, pattern match the result, and assert.
This guide runs entirely locally. No provider keys or network calls are required.
Actions are pure functions. Test them by calling run/2 directly with a params map and a context map.
defmodule MyApp.IncrementAction do
use Jido.Action,
name: "increment",
description: "Increments a counter",
schema: [
by: [type: :integer, default: 1, doc: "Amount to increment by"]
]
@impl true
def run(%{by: amount}, context) do
current = Map.get(context.state, :count, 0)
{:ok, %{count: current + amount}}
end
end
Pass the validated params and a context map containing the state your action reads from.
assert {:ok, %{count: 5}} =
MyApp.IncrementAction.run(%{by: 5}, %{state: %{count: 0}})
assert {:ok, %{count: 13}} =
MyApp.IncrementAction.run(%{by: 3}, %{state: %{count: 10}})
Define an action that rejects invalid input and test the error path.
defmodule MyApp.DivideAction do
use Jido.Action,
name: "divide",
description: "Divides value by divisor",
schema: [
divisor: [type: :integer, required: true, doc: "Divisor"]
]
@impl true
def run(%{divisor: 0}, _context), do: {:error, :division_by_zero}
def run(%{divisor: d}, context) do
value = Map.get(context.state, :value, 100)
{:ok, %{value: div(value, d)}}
end
end
assert {:error, :division_by_zero} =
MyApp.DivideAction.run(%{divisor: 0}, %{state: %{}})
assert {:ok, %{value: 50}} =
MyApp.DivideAction.run(%{divisor: 2}, %{state: %{value: 100}})
Define an agent and exercise it with cmd/2. Every call returns {agent, directives} where agent is a new immutable struct with updated state.
defmodule MyApp.CounterAgent do
use Jido.Agent,
name: "counter_agent",
description: "Counts things",
schema: [
count: [type: :integer, default: 0]
]
end
agent = MyApp.CounterAgent.new()
assert agent.state.count == 0
agent = MyApp.CounterAgent.new(state: %{count: 10})
assert agent.state.count == 10
agent = MyApp.CounterAgent.new()
{agent, _directives} =
MyApp.CounterAgent.cmd(agent, {MyApp.IncrementAction, %{by: 3}})
assert agent.state.count == 3
State accumulates across sequential calls. Each cmd/2 returns a fresh struct.
agent = MyApp.CounterAgent.new()
{agent, _} = MyApp.CounterAgent.cmd(agent, {MyApp.IncrementAction, %{by: 2}})
{agent, _} = MyApp.CounterAgent.cmd(agent, {MyApp.IncrementAction, %{by: 5}})
assert agent.state.count == 7
Override the agent ID for deterministic test assertions.
agent = MyApp.CounterAgent.new(id: "test-counter-1")
assert agent.id == "test-counter-1"
cmd/2 returns a list of directive structs alongside the updated agent. Directives describe external effects the runtime should execute - they are bare structs, not wrapped in tuples.
alias Jido.Agent.Directive
defmodule MyApp.EmitAction do
use Jido.Action,
name: "emit_result",
description: "Emits a signal with the current count",
schema: []
@impl true
def run(_params, context) do
signal = Jido.Signal.new!("counter.updated", %{count: context.state.count}, source: "/counter")
{:ok, %{}, [Directive.emit(signal)]}
end
end
agent = MyApp.CounterAgent.new(state: %{count: 42})
{_agent, directives} = MyApp.CounterAgent.cmd(agent, MyApp.EmitAction)
assert [%Directive.Emit{signal: signal}] = directives
assert signal.type == "counter.updated"
assert signal.data.count == 42
When an action fails validation or returns an error, cmd/2 emits an Error directive instead of raising.
defmodule MyApp.BadAction do
use Jido.Action,
name: "bad_action",
description: "Always fails",
schema: []
@impl true
def run(_params, _context), do: {:error, :something_went_wrong}
end
agent = MyApp.CounterAgent.new()
{_agent, directives} = MyApp.CounterAgent.cmd(agent, MyApp.BadAction)
assert [%Directive.Error{error: error}] = directives
assert error.class == :execution
assert error.phase == :execution
Most actions produce no directives. Assert on the empty list to confirm no side effects.
agent = MyApp.CounterAgent.new()
{agent, directives} = MyApp.CounterAgent.cmd(agent, {MyApp.IncrementAction, %{by: 1}})
assert directives == []
assert agent.state.count == 1
When you need to test signal routing, process lifecycle, or async behavior, start the agent in an AgentServer.
{:ok, pid} =
Jido.start_agent(runtime, MyApp.CounterAgent)
state/1 returns the full server state struct. The agent struct lives at state.agent.
{:ok, server_state} = Jido.AgentServer.state(pid)
assert server_state.agent.state.count == 0
call/2 sends a signal and waits for processing. It returns the updated agent struct.
defmodule MyApp.SignalCounterAgent do
use Jido.Agent,
name: "signal_counter",
description: "Routes increment signals",
schema: [
count: [type: :integer, default: 0]
]
@impl true
def signal_routes(_ctx) do
[{"counter.increment", MyApp.IncrementAction}]
end
end
{:ok, pid} =
Jido.start_agent(runtime, MyApp.SignalCounterAgent)
signal = Jido.Signal.new!("counter.increment", %{by: 10}, source: "/test")
{:ok, agent} = Jido.AgentServer.call(pid, signal)
assert agent.state.count == 10
cast/2 returns :ok immediately. Query state after a short wait to verify processing.
signal = Jido.Signal.new!("counter.increment", %{by: 5}, source: "/test")
:ok = Jido.AgentServer.cast(pid, signal)
Process.sleep(100)
{:ok, server_state} = Jido.AgentServer.state(pid)
assert server_state.agent.state.count == 15
Debug mode records internal events in a ring buffer. Use it to verify that signals were received and directives were processed without inspecting internal state.
{:ok, pid} = Jido.start_agent(
runtime,
MyApp.SignalCounterAgent,
debug: true
)
:ok = Jido.AgentServer.set_debug(pid, true)
Each event has :at (monotonic timestamp in ms), :type (atom), and :data (map).
signal = Jido.Signal.new!("counter.increment", %{by: 1}, source: "/test")
{:ok, _agent} = Jido.AgentServer.call(pid, signal)
{:ok, events} = Jido.AgentServer.recent_events(pid, limit: 10)
types = Enum.map(events, & &1.type)
assert :signal_received in types
recent_events/2 returns an error when debug mode is off. Use this to confirm your test setup.
{:ok, pid} = Jido.start_agent(
runtime,
MyApp.CounterAgent
)
assert {:error, :debug_not_enabled} =
Jido.AgentServer.recent_events(pid, limit: 5)
These patterns translate directly into ExUnit test files in a Mix project.
defmodule MyApp.CounterAgentTest do
use ExUnit.Case, async: true
alias MyApp.{CounterAgent, IncrementAction}
describe "state transitions" do
test "increments count" do
agent = CounterAgent.new()
{agent, _} = CounterAgent.cmd(agent, {IncrementAction, %{by: 3}})
assert agent.state.count == 3
end
end
end
Verify that your agent maps signal types to the correct actions.
defmodule MyApp.SignalCounterAgentTest do
use ExUnit.Case, async: true
test "routes counter.increment to IncrementAction" do
agent = MyApp.SignalCounterAgent.new()
routes = MyApp.SignalCounterAgent.signal_routes(%{agent: agent})
assert {"counter.increment", MyApp.IncrementAction} in routes
end
end
For tests that need a running agent server, start the instance in a setup block.
defmodule MyApp.CounterServerTest do
use ExUnit.Case, async: false
setup do
{:ok, _} = Jido.start()
{:ok, pid} = Jido.start_agent(
Jido.default_instance(),
MyApp.SignalCounterAgent
)
%{pid: pid}
end
test "processes signals", %{pid: pid} do
signal = Jido.Signal.new!("counter.increment", %{by: 7}, source: "/test")
{:ok, agent} = Jido.AgentServer.call(pid, signal)
assert agent.state.count == 7
end
end
Now that you have test patterns for agents and actions, explore related topics.