Jido testing
Agent Skills documentation and examples for the Jido ecosystem
npx -y skills add agentjido/jido-skills --skill jido-testingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Test Jido agents and actions effectively without OTP processes. Use when writing tests for agents, actions, plugins, or signal handling. Covers unit testing, property-based testing, and integration testing patterns.
The file declares its own license as Apache-2.0. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
10.7 KB, as published. Nobody here has run it
Testing Jido Agents and Actions
MANDATORY PREPARATION
Invoke /jido-core first — it contains ecosystem context, conventions, and core design principles. Follow its guidance before proceeding.
Core Principle
Jido agents are pure data — test them without GenServer. Call cmd/2 on
agent structs, assert state changes and directives. Only use AgentServer
for integration tests that need signal routing and directive execution.
Step 1: Testing Actions
Actions are the simplest unit to test. Call run/2 directly:
defmodule MyApp.Actions.IncrementTest do
use ExUnit.Case, async: true
alias MyApp.Actions.Increment
test "increments counter by amount" do
params = %{amount: 5}
context = %{state: %{counter: 10}}
assert {:ok, %{counter: 15}} = Increment.run(params, context)
end
test "defaults amount to 1" do
params = %{}
context = %{state: %{counter: 0}}
assert {:ok, %{counter: 1}} = Increment.run(params, context)
end
test "rejects negative amounts" do
assert {:error, _reason} = Increment.validate_params(%{amount: -1})
end
end
Validate Params and Output
test "validates input schema" do
assert {:ok, %{input: "hello"}} = MyAction.validate_params(%{input: "hello"})
assert {:error, _} = MyAction.validate_params(%{input: 123})
end
test "validates output schema" do
assert {:ok, %{result: "ok"}} = MyAction.validate_output(%{result: "ok", byte_count: 2})
end
Execute Through Jido.Exec
For full execution with validation and lifecycle hooks:
test "full execution pipeline" do
assert {:ok, result} = Jido.Exec.run(MyAction, %{input: "test"}, %{})
assert result.processed == true
end
Step 2: Testing Agents
Test agents as pure functions using cmd/2 on structs:
defmodule MyApp.CounterAgentTest do
use ExUnit.Case, async: true
alias MyApp.CounterAgent
alias MyApp.Actions.{Increment, Decrement}
setup do
agent = CounterAgent.new()
%{agent: agent}
end
test "new agent has default state", %{agent: agent} do
assert agent.state.counter == 0
assert agent.state.status == :idle
end
test "cmd/2 with single action", %{agent: agent} do
{agent, directives} = CounterAgent.cmd(agent, {Increment, %{amount: 5}})
assert agent.state.counter == 5
assert directives == []
end
test "cmd/2 with action list", %{agent: agent} do
{agent, directives} = CounterAgent.cmd(agent, [
{Increment, %{amount: 3}},
{Increment, %{amount: 7}}
])
assert agent.state.counter == 10
assert directives == []
end
test "cmd/2 returns directives for effects", %{agent: agent} do
{_agent, directives} = CounterAgent.cmd(agent, NotifyAction)
assert [%Jido.Agent.Directive.Emit{} | _] = directives
end
test "set/2 updates state", %{agent: agent} do
{:ok, agent} = CounterAgent.set(agent, %{counter: 42})
assert agent.state.counter == 42
end
end
Testing with Custom Initial State
test "agent with initial state" do
agent = CounterAgent.new(state: %{counter: 100, status: :active})
assert agent.state.counter == 100
{agent, _} = CounterAgent.cmd(agent, {Decrement, %{amount: 25}})
assert agent.state.counter == 75
end
Step 3: Testing Lifecycle Hooks
test "on_before_cmd is called before processing" do
agent = HookAgent.new()
{agent, _directives} = HookAgent.cmd(agent, SomeAction)
# Verify hook side-effects in state
assert agent.state.hook_called == true
end
test "on_after_cmd can transform directives" do
agent = HookAgent.new()
{_agent, directives} = HookAgent.cmd(agent, SomeAction)
# Verify directives were transformed by hook
assert length(directives) > 0
end
Step 4: Testing Plugins
Verify plugin state isolation and signal handling:
defmodule MyApp.AuditPluginTest do
use ExUnit.Case, async: true
test "plugin state is isolated under its state_key" do
agent = AgentWithAuditPlugin.new()
# Plugin state is nested under its key
assert agent.state.__audit_log__ == %{entries: []}
end
test "plugin signal handling" do
agent = AgentWithAuditPlugin.new()
{agent, _} = AgentWithAuditPlugin.cmd(agent, SomeTrackedAction)
# Verify plugin transformed state
assert length(agent.state.__audit_log__.entries) > 0
end
end
Step 5: Property-Based Testing with StreamData
Use StreamData for generative testing of actions:
defmodule MyApp.Actions.MathTest do
use ExUnit.Case, async: true
use ExUnitProperties
alias MyApp.Actions.Add
property "addition is commutative" do
check all a <- integer(),
b <- integer() do
assert {:ok, %{result: r1}} = Add.run(%{a: a, b: b}, %{})
assert {:ok, %{result: r2}} = Add.run(%{a: b, b: a}, %{})
assert r1 == r2
end
end
property "counter never goes negative with abs_increment" do
check all amounts <- list_of(positive_integer(), min_length: 1) do
agent = CounterAgent.new()
agent =
Enum.reduce(amounts, agent, fn amount, acc ->
{acc, _} = CounterAgent.cmd(acc, {Increment, %{amount: amount}})
acc
end)
assert agent.state.counter > 0
end
end
end
Step 6: Integration Testing with AgentServer
For tests that need the full OTP runtime, use JidoTest.Case:
defmodule MyApp.CounterIntegrationTest do
use JidoTest.Case, async: true
alias Jido.AgentServer
alias MyApp.CounterAgent
test "signal routing through AgentServer", %{jido: jido} do
pid = start_server(%{jido: jido}, CounterAgent)
# Create and send a signal
sig = signal("increment", %{amount: 5})
:ok = AgentServer.call(pid, sig, 5_000)
# Check agent state
{:ok, state} = AgentServer.state(pid)
assert state.agent.state.counter == 5
end
test "async signal processing", %{jido: jido} do
pid = start_server(%{jido: jido}, CounterAgent)
sig = signal("increment", %{amount: 1})
:ok = AgentServer.cast(pid, sig)
# Use eventually for async assertions
assert_eventually fn ->
{:ok, state} = AgentServer.state(pid)
state.agent.state.counter == 1
end
end
end
JidoTest.Case Helpers
| Helper | Description |
|---|---|
start_server/3 | Starts AgentServer with auto-cleanup |
signal/3 | Creates a test signal with defaults |
unique_id/1 | Generates unique ID with prefix |
test_registry/1 | Returns registry for test's Jido instance |
assert_eventually/1 | Polling assertion for async results |
Each test gets an isolated Jido instance via the %{jido: jido} context.
Step 7: Mocking External Dependencies
Use Mimic for mocking external services:
# test/test_helper.exs
Mimic.copy(MyApp.ExternalAPI)
# In test
defmodule MyApp.WeatherActionTest do
use ExUnit.Case, async: true
use Mimic
test "handles API errors gracefully" do
stub(MyApp.ExternalAPI, :fetch_weather, fn _location ->
{:error, :timeout}
end)
assert {:error, _} = MyApp.Actions.GetWeather.run(%{location: "Tokyo"}, %{})
end
test "returns weather data on success" do
expect(MyApp.ExternalAPI, :fetch_weather, fn "Tokyo" ->
{:ok, %{temp: 22, conditions: "sunny"}}
end)
assert {:ok, %{temperature: 22}} = MyApp.Actions.GetWeather.run(%{location: "Tokyo"}, %{})
end
end
Step 8: Running the Test Suite
# Run all tests
mix test
# Run with coverage
mix test --cover
# Run specific test file
mix test test/my_agent_test.exs
# Run specific test by line
mix test test/my_agent_test.exs:42
# Full quality check (formatting, credo, dialyzer, tests)
mix quality
Complete Example: Counter Agent Test Module
defmodule MyApp.CounterTest do
use ExUnit.Case, async: true
defmodule IncrementAction do
use Jido.Action,
name: "increment",
schema: [amount: [type: :integer, default: 1]]
@impl true
def run(params, context) do
{:ok, %{counter: (context.state[:counter] || 0) + params.amount}}
end
end
defmodule DecrementAction do
use Jido.Action,
name: "decrement",
schema: [amount: [type: :integer, default: 1]]
@impl true
def run(params, context) do
{:ok, %{counter: (context.state[:counter] || 0) - params.amount}}
end
end
defmodule CounterAgent do
use Jido.Agent,
name: "test_counter",
schema: [counter: [type: :integer, default: 0]]
def signal_routes(_ctx) do
[
{"increment", IncrementAction},
{"decrement", DecrementAction}
]
end
end
setup do
%{agent: CounterAgent.new()}
end
test "starts at zero", %{agent: agent} do
assert agent.state.counter == 0
end
test "increments by default amount", %{agent: agent} do
{agent, []} = CounterAgent.cmd(agent, IncrementAction)
assert agent.state.counter == 1
end
test "increments by custom amount", %{agent: agent} do
{agent, []} = CounterAgent.cmd(agent, {IncrementAction, %{amount: 10}})
assert agent.state.counter == 10
end
test "decrements", %{agent: agent} do
{agent, []} = CounterAgent.cmd(agent, {IncrementAction, %{amount: 5}})
{agent, []} = CounterAgent.cmd(agent, {DecrementAction, %{amount: 3}})
assert agent.state.counter == 2
end
test "chains multiple actions", %{agent: agent} do
{agent, []} = CounterAgent.cmd(agent, [
{IncrementAction, %{amount: 10}},
{DecrementAction, %{amount: 3}},
{IncrementAction, %{amount: 1}}
])
assert agent.state.counter == 8
end
end
DO / DON'T
DO
- Test agents as pure data first. Use
cmd/2on structs before involving AgentServer. - Use
async: trueon all tests that don't share mutable state. - Use
JidoTest.Casefor integration tests — it provides isolated Jido instances. - Use
assert_eventuallyfor async assertions instead ofProcess.sleep. - Test actions in isolation with
run/2before testing inside agents.
DON'T
- Start GenServers in unit tests. Test pure logic first.
- Use
Process.sleepfor timing — useassert_eventuallyor proper await. - Share state between tests. Each test should create its own agent struct.
- Skip schema validation tests. Verify your schemas reject bad input.
- Mock internal Jido modules. Mock external boundaries only.