Testing Your Application

Copy Markdown View Source

This guide covers strategies for testing code that uses GeminiCliSdk.

Mock CLI Approach

GeminiCliSdk resolves the CLI binary via the GEMINI_CLI_PATH environment variable. You can point this at a mock script for testing:

defmodule MyApp.GeminiTest do
  use ExUnit.Case, async: false

  setup do
    dir = Path.join(System.tmp_dir!(), "my_test_#{System.unique_integer([:positive])}")
    File.mkdir_p!(dir)

    script = """
    #!/usr/bin/env bash
    cat > /dev/null
    echo '{"type":"init","session_id":"test","model":"test"}'
    echo '{"type":"message","role":"assistant","content":"Mock response","delta":true}'
    echo '{"type":"result","status":"success","stats":{"total_tokens":10}}'
    """

    path = Path.join(dir, "gemini")
    File.write!(path, script)
    File.chmod!(path, 0o755)

    on_exit(fn -> File.rm_rf(dir) end)

    %{stub_path: path}
  end

  test "my feature uses Gemini", %{stub_path: stub_path} do
    original = System.get_env("GEMINI_CLI_PATH")
    System.put_env("GEMINI_CLI_PATH", stub_path)

    try do
      {:ok, result} = MyApp.ask_gemini("test question")
      assert result =~ "Mock response"
    after
      if original, do: System.put_env("GEMINI_CLI_PATH", original),
        else: System.delete_env("GEMINI_CLI_PATH")
    end
  end
end

JSONL Fixtures

For more realistic tests, create JSONL fixture files that mirror real CLI output:

{"type":"init","timestamp":"2026-01-01T00:00:00Z","session_id":"test-001","model":"gemini-2.5-pro"}
{"type":"message","role":"user","content":"hello","timestamp":"2026-01-01T00:00:01Z"}
{"type":"message","role":"assistant","content":"Hello! How can I help?","delta":true,"timestamp":"2026-01-01T00:00:02Z"}
{"type":"result","status":"success","stats":{"total_tokens":50,"input_tokens":10,"output_tokens":40,"duration_ms":500,"tool_calls":0},"timestamp":"2026-01-01T00:00:03Z"}

Then serve them from your mock script:

#!/usr/bin/env bash
cat > /dev/null
while IFS= read -r line || [ -n "$line" ]; do
  echo "$line"
done < "$GEMINI_TEST_STREAM_FILE"

The stream event parser preserves unknown fields in each event struct's extra map. Fixture-based tests are a good place to assert that future wire fields round-trip without breaking the known contract.

Wrapping the SDK

Consider wrapping GeminiCliSdk behind a behaviour for easier testing:

defmodule MyApp.AI do
  @callback ask(String.t()) :: {:ok, String.t()} | {:error, term()}
end

defmodule MyApp.AI.Gemini do
  @behaviour MyApp.AI

  @impl true
  def ask(prompt) do
    GeminiCliSdk.run(prompt, %GeminiCliSdk.Options{model: GeminiCliSdk.Models.fast_model()})
  end
end

defmodule MyApp.AI.Mock do
  @behaviour MyApp.AI

  @impl true
  def ask(_prompt), do: {:ok, "Mock response"}
end

Then in your application code:

defmodule MyApp.Feature do
  @ai_module Application.compile_env(:my_app, :ai_module, MyApp.AI.Gemini)

  def process(input) do
    @ai_module.ask("Process: #{input}")
  end
end

Live Integration Tests

For tests that run against the real CLI, tag them and exclude by default:

# test/test_helper.exs
ExUnit.start(exclude: [:live])

# test/live/gemini_live_test.exs
defmodule GeminiLiveTest do
  use ExUnit.Case, async: false

  @moduletag :live

  test "real CLI responds" do
    {:ok, response} = GeminiCliSdk.run("Say hello")
    assert is_binary(response)
    assert byte_size(response) > 0
  end
end

Run live tests with:

mix test --only live