ClaudeCode.Test (ClaudeCode v0.16.0)

View Source

Req.Test-style test helpers for ClaudeCode.

This module provides a simple way to mock Claude responses in your tests, following the same patterns as Req.Test.

Setup

  1. Configure the adapter in your test environment:

     # config/test.exs
     config :claude_code, adapter: {ClaudeCode.Test, ClaudeCode}
  2. In your test helper, start the ownership server:

     # test/test_helper.exs
     ExUnit.start()
     Supervisor.start_link([ClaudeCode.Test], strategy: :one_for_one)
  3. Register stubs in your tests:

     test "returns greeting" do
       ClaudeCode.Test.stub(ClaudeCode, fn _query, _opts ->
         [
           ClaudeCode.Test.text("Hello! How can I help?"),
           ClaudeCode.Test.result()
         ]
       end)
    
       {:ok, session} = ClaudeCode.start_link([])
       result = session |> ClaudeCode.stream("Hi") |> ClaudeCode.Stream.final_text()
       assert result == "Hello! How can I help?"
     end

Message Helpers

  • text/2 - Creates an assistant message with text content
  • tool_use/3 - Creates a tool invocation message
  • tool_result/2 - Creates a tool result message
  • thinking/2 - Creates a thinking block message
  • result/2 - Creates the final result message
  • system/1 - Creates a system initialization message

Async Tests

This module uses NimbleOwnership for process-based isolation, allowing concurrent test execution. Stubs registered in a test process are only visible to that process and its allowees.

To allow a spawned process to access stubs:

ClaudeCode.Test.allow(ClaudeCode, self(), pid_of_spawned_process)

Using Different Names

The name in {ClaudeCode.Test, name} can be any term. This is useful when you need different stub behaviors in the same test, or when building wrapper modules around ClaudeCode:

# Testing multiple "agents" with different behaviors
ClaudeCode.Test.stub(MyApp.CodingAgent, fn _query, _opts ->
  [ClaudeCode.Test.text("Here's the code...")]
end)

ClaudeCode.Test.stub(MyApp.ResearchAgent, fn _query, _opts ->
  [ClaudeCode.Test.text("Based on my research...")]
end)

{:ok, coder} = ClaudeCode.start_link(adapter: {ClaudeCode.Test, MyApp.CodingAgent})
{:ok, researcher} = ClaudeCode.start_link(adapter: {ClaudeCode.Test, MyApp.ResearchAgent})

Summary

Functions

Allows pid_to_allow to access stubs owned by owner_pid.

Creates a final result message.

Sets the mode to shared global.

Returns a list of messages from the registered stub.

Registers a stub for the given name.

Creates a system initialization message.

Creates an assistant message with text content.

Creates an assistant message with a thinking block.

Creates a user message with a tool result block.

Creates an assistant message with a tool use block.

Functions

allow(name, owner_pid, pid_to_allow)

@spec allow(name :: term(), owner_pid :: pid(), pid_to_allow :: pid()) ::
  :ok | {:error, term()}

Allows pid_to_allow to access stubs owned by owner_pid.

This is useful when you spawn processes that need to access the same stubs as the test process.

Example

test "spawned process can use stub" do
  ClaudeCode.Test.stub(ClaudeCode, fn _, _ -> [...] end)

  task = Task.async(fn ->
    # This task can now access the stub
    {:ok, session} = ClaudeCode.start_link([])
    ClaudeCode.stream(session, "hi") |> Enum.to_list()
  end)

  # Allow the task to access our stubs
  ClaudeCode.Test.allow(ClaudeCode, self(), task.pid)

  Task.await(task)
end

result(result_text \\ "Done", opts \\ [])

Creates a final result message.

Options

  • :is_error - Whether this is an error result (default: false)
  • :subtype - Result subtype (default: :success or :error_during_execution)
  • :session_id - Session ID (default: auto-generated)
  • :duration_ms - Duration in milliseconds (default: 100)
  • :num_turns - Number of turns (default: 1)

Examples

ClaudeCode.Test.result()
ClaudeCode.Test.result("Task completed successfully")
ClaudeCode.Test.result("Rate limit exceeded", is_error: true)

set_mode_to_shared()

@spec set_mode_to_shared() :: :ok

Sets the mode to shared global.

In shared mode, all processes can access stubs without explicit allowances. This is useful for integration tests or when process ownership is complex.

Example

setup do
  ClaudeCode.Test.set_mode_to_shared()
  :ok
end

stream(name, query, opts, callers \\ nil)

Returns a list of messages from the registered stub.

Called by ClaudeCode.Adapter.Test to retrieve stub messages. The optional callers argument allows passing the caller chain from a different process (used by the test adapter).

stub(name, fun_or_messages)

@spec stub(
  name :: term(),
  fun_or_messages :: (String.t(), keyword() -> [term()]) | [term()]
) :: :ok

Registers a stub for the given name.

The stub can be either a function or a list of messages:

Function stub

Receives the query and options, returns a list of messages:

ClaudeCode.Test.stub(ClaudeCode, fn query, opts ->
  [
    ClaudeCode.Test.text("Response to: #{query}"),
    ClaudeCode.Test.result()
  ]
end)

Static stub

A list of messages that will be returned for any query:

ClaudeCode.Test.stub(ClaudeCode, [
  ClaudeCode.Test.text("Static response"),
  ClaudeCode.Test.result()
])

system(opts \\ [])

Creates a system initialization message.

Options

  • :session_id - Session ID (default: auto-generated)
  • :model - Model name (default: "claude-sonnet-4-20250514")
  • :tools - List of available tools (default: [])
  • :cwd - Current working directory (default: "/test")

Examples

ClaudeCode.Test.system()
ClaudeCode.Test.system(model: "claude-opus-4-20250514", tools: ["Read", "Edit"])

text(text, opts \\ [])

Creates an assistant message with text content.

Options

  • :session_id - Session ID (default: auto-generated)
  • :stop_reason - Stop reason atom (default: nil)
  • :message_id - Message ID (default: auto-generated)

Examples

ClaudeCode.Test.text("Hello world!")
ClaudeCode.Test.text("Done", stop_reason: :end_turn)

thinking(thinking_text, opts \\ [])

Creates an assistant message with a thinking block.

Options

  • :signature - Thinking signature (default: auto-generated)
  • :text - Optional text to include after thinking
  • :session_id - Session ID (default: auto-generated)

Examples

ClaudeCode.Test.thinking("Let me analyze this step by step...")
ClaudeCode.Test.thinking("First...", text: "Here's my answer")

tool_result(content \\ "", opts \\ [])

@spec tool_result(
  String.t() | map(),
  keyword()
) :: ClaudeCode.Message.UserMessage.t()

Creates a user message with a tool result block.

The content can be a string or a map. Maps are automatically JSON-encoded. Content is wrapped as a list of content blocks: [%{"type" => "text", "text" => content}]

Options

  • :tool_use_id - ID of the tool use this is responding to (default: nil for auto-linking)
  • :is_error - Whether the tool execution failed (default: false)
  • :session_id - Session ID (default: auto-generated)

Examples

ClaudeCode.Test.tool_result("file contents here")
ClaudeCode.Test.tool_result("Permission denied", is_error: true)
ClaudeCode.Test.tool_result(%{status: "success", data: [1, 2, 3]})

tool_use(name, input, opts \\ [])

Creates an assistant message with a tool use block.

Options

  • :id - Tool use ID (default: auto-generated)
  • :text - Optional text to include before the tool use
  • :session_id - Session ID (default: auto-generated)

Examples

ClaudeCode.Test.tool_use("Read", %{path: "/tmp/file.txt"})
ClaudeCode.Test.tool_use("Bash", %{command: "ls -la"}, text: "Let me check...")