# `ExGram.Test`
[🔗](https://github.com/rockneurotiko/ex_gram/blob/0.64.0/lib/ex_gram/test.ex#L1)

ExGram testing conveniences.

This module provides a unified interface for all testing utilities, delegating to
`ExGram.Adapter.Test` and `ExGram.Updates.Test`.

## Overview

ExGram ships with a test adapter that intercepts Telegram API calls, making it easy
to test bots without hitting real servers. The adapter supports per-process isolation
for async tests and provides stub/expect/verify semantics similar to [Mox](https://hexdocs.pm/mox).

## Quick Start

Configure your test environment in `config/test.exs`:

    config :ex_gram,
      token: "test_token",
      adapter: ExGram.Adapter.Test,
      updates: ExGram.Updates.Test

Start the adapter in `test_helper.exs`:

    {:ok, _} = ExGram.Adapter.Test.start_link()
    ExUnit.start()

The recommended way to use this module is via `use ExGram.Test` in your test module.
This sets up `set_from_context` and `verify_on_exit!` automatically:

    defmodule MyBotTest do
      use ExUnit.Case, async: true
      use ExGram.Test

      setup context do
        {bot_name, _} = ExGram.Test.start_bot(context, MyApp.Bot)
        {:ok, bot_name: bot_name}
      end

      test "sends welcome message", %{bot_name: bot_name} do
        ExGram.Test.expect(:send_message, %{message_id: 1, text: "Welcome!"})
        ExGram.Test.push_update(bot_name, build_update("/start"))
      end
    end

## `use ExGram.Test`

Calling `use ExGram.Test` in your test module registers a `setup` callback that:

- Calls `set_from_context/1` to automatically pick private or global mode based on
  whether the test is `async: true` or `async: false`.
- Calls `verify_on_exit!/1` so expectations are verified automatically when the test exits.

Options:

  * `:set_from_context` - whether to call `set_from_context/1` in setup (default: `true`)
  * `:verify_on_exit` - whether to call `verify_on_exit!/1` in setup (default: `true`)

## Process isolation and `start_bot/3`

`start_bot/3` creates an isolated, uniquely named bot for the current test. It also
ensures the bot's Dispatcher and Updates worker processes are immediately allowed to
use the test's stubs, without any manual `allow/2` call.

This works by subscribing to the `[:ex_gram, :bot, :init, :start]` and
`[:ex_gram, :updates, :init, :start]` telemetry events emitted synchronously during process
startup. When those events fire, the processes are automatically allowed under the calling
test's ownership. The telemetry handler is scoped to the specific `bot_name` so concurrent
async tests never cross-allow each other's processes. The handler is detached automatically
via `on_exit` when the test exits.

## Stubbing

Stubs define responses that persist for all matching calls:

    # Static response
    ExGram.Test.stub(:send_message, %{message_id: 1, text: "ok"})

    # Dynamic response based on request body
    ExGram.Test.stub(:send_message, fn body ->
      {:ok, %{message_id: 1, chat_id: body["chat_id"], text: "ok"}}
    end)

    # Catch-all for all actions
    ExGram.Test.stub(fn action, body ->
      case action do
        :send_message -> {:ok, %{message_id: 1, text: "ok"}}
        :get_me -> {:ok, %{id: 1, is_bot: true}}
      end
    end)

## Expectations

Expectations are consumed after being called and can be verified:

    # Consumed after 1 call
    ExGram.Test.expect(:send_message, %{message_id: 1, text: "ok"})

    # Consumed after N calls
    ExGram.Test.expect(:send_message, 3, %{message_id: 1, text: "ok"})

    # Verify all expectations were met
    ExGram.Test.verify!()

## Testing Bots

Start an isolated bot instance for each test with `start_bot/3`, then push updates
using `push_update/2`. By default `start_bot/3` sets `handler_mode: :sync`, so
`push_update/2` only returns after the bot's handler has fully executed - no sleeps
or polling needed.

    {bot_name, _} = ExGram.Test.start_bot(context, MyApp.Bot)

    ExGram.Test.expect(:send_message, %{message_id: 1, text: "Welcome!"})

    update = %ExGram.Model.Update{
      update_id: 1,
      message: %ExGram.Model.Message{
        message_id: 100,
        chat: %ExGram.Model.Chat{id: 123, type: "private"},
        text: "/start"
      }
    }

    # Blocks until the handler completes; expectation is consumed when this returns
    ExGram.Test.push_update(bot_name, update)

See the [Testing guide](testing.md) for more examples and patterns.

# `allow`

Allow a spawned process to access the current test's stubs and expectations.

## Example

    test "spawned process can use stubs" do
      ExGram.Test.stub(:send_message, %{message_id: 1, text: "ok"})

      task = Task.async(fn ->
        ExGram.send_message(123, "From task")
      end)

      ExGram.Test.allow(self(), task.pid)

      {:ok, msg} = Task.await(task)
      assert msg.message_id == 1
    end

# `clean`

Clean the current process's stubs, expectations, and recorded calls.

Useful in setup blocks if you need to reset state:

## Example

    setup do
      ExGram.Test.clean()
      :ok
    end

# `expect`

Expect a catch-all response with a callback, consumed after 1 call.

## Example

    ExGram.Test.expect(fn action, body ->
      assert action == :send_message
      {:ok, %{message_id: 1, text: "ok"}}
    end)

# `expect`

Expect a response for a specific action, consumed after 1 call.

## Examples

    ExGram.Test.expect(:send_message, %{message_id: 1, text: "ok"})

    ExGram.Test.expect(:send_message, fn body ->
      assert body[:text] == "Hello"
      {:ok, %{message_id: 1, text: "ok"}}
    end)

# `expect`

Expect a response for a specific action, consumed after N calls.

## Example

    ExGram.Test.expect(:send_message, 3, %{message_id: 1, text: "ok"})

# `get_calls`

Get all recorded API calls as a list of `{verb, action, body}` tuples.

## Example

    calls = ExGram.Test.get_calls()
    assert length(calls) == 2

    {verb, action, body} = hd(calls)
    assert verb == :post
    assert action == :send_message
    assert body["chat_id"] == 123

# `handle_event_allow_pid`

# `push_update`

Push a test update to a bot's dispatcher.

Simulates an incoming update from Telegram. The bot's processes are already
allowed to use the test's stubs from `start_bot/3`, so no additional `allow/2`
call is needed before calling this function.

When the bot was started with `handler_mode: :sync` (the default from `start_bot/3`),
this call blocks until the bot's handler has fully executed, including all API calls.
Expectations are consumed and calls are recorded by the time this function returns,
so you can assert on results immediately after.

When the bot was started with `handler_mode: :async`, the update is enqueued and
this function returns before the handler runs.

## Example

    test "bot responds to /start", %{bot_name: bot_name} do
      ExGram.Test.expect(:send_message, fn body ->
        assert body[:text] =~ "Welcome"
        {:ok, %{message_id: 1, chat: %{id: 123, type: "private"}, text: "Welcome!"}}
      end)

      update = %ExGram.Model.Update{
        update_id: 1,
        message: %ExGram.Model.Message{
          message_id: 100,
          date: 1_700_000_000,
          chat: %ExGram.Model.Chat{id: 123, type: "private"},
          text: "/start"
        }
      }

      # With handler_mode: :sync (default), the handler has run by the time this returns
      ExGram.Test.push_update(bot_name, update)
    end

# `set_from_context`

Set the adapter to private or global mode depending on the current test context.

# `set_global`

Set the adapter to global mode (shared stubs across all processes).

Only use this for synchronous tests. Prefer `allow/2` for async tests.

## Example

    setup do
      ExGram.Test.set_global()
      on_exit(fn -> ExGram.Test.set_private() end)
    end

# `set_private`

Set the adapter to private mode (per-process isolation).

This is the default mode.

# `start_bot`

Start an isolated bot instance for a test.

Creates a uniquely named bot process derived from the test name so that multiple
tests can run concurrently without colliding. The bot is started under a unique
module name and registered under a unique atom (`bot_name`), which is also the
name passed to `push_update/2`.

Returns `{bot_name, module_name}`, where `bot_name` is the Dispatcher's registered
name (the process that handles all updates) and `module_name` is the Supervisor's
name (typically not needed, but useful for debugging or stopping the bot).

## Process isolation

`start_bot/3` automatically allows the bot's Dispatcher and Updates worker processes
to use the calling test's stubs and expectations. This is done by subscribing to the
`[:ex_gram, :bot, :init, :start]` and `[:ex_gram, :updates, :init, :start]` telemetry
events, which fire synchronously during startup. The telemetry handler is scoped to this
bot's name, so concurrent tests never accidentally allow each other's processes. The
handler is detached automatically on test exit.

No manual `allow/2` call is needed for the standard `push_update/2` workflow.

## Options

The following options are merged on top of the test defaults:

  * `:method` - Updates source, defaults to `:test` (`ExGram.Updates.Test`)
  * `:token` - Bot token, defaults to `"test_token"`
  * `:get_me` - Whether to call `get_me` on startup, defaults to `false` (skips the API call)
  * `:setup_commands` - Whether to register commands on startup, defaults to `false`
  * `:handler_mode` - How the dispatcher executes the handler. `:sync` (default)
    runs the handler inline so `push_update/2` blocks until it completes. `:async`
    spawns a new process (the production default) and returns immediately.
  * `:extra_info` - Map of extra data passed to the bot's context. The test process
    PID is always injected as `:test_pid`.

## Example

    setup context do
      {bot_name, _} = ExGram.Test.start_bot(context, MyApp.Bot)
      {:ok, bot_name: bot_name}
    end

    test "responds to /start", %{bot_name: bot_name} do
      ExGram.Test.expect(:send_message, %{message_id: 1, text: "Welcome!"})
      ExGram.Test.push_update(bot_name, build_update("/start"))
    end

# `stub`

Stub a catch-all response with a callback that receives action and body.

## Example

    ExGram.Test.stub(fn action, body ->
      case action do
        :send_message -> {:ok, %{message_id: 1, text: "ok"}}
        :get_me -> {:ok, %{id: 1, is_bot: true}}
      end
    end)

# `stub`

Stub a response for a specific action or path.

The response can be a static value (wrapped in `{:ok, value}`) or a callback
that receives the request body.

## Examples

    ExGram.Test.stub(:send_message, %{message_id: 1, text: "ok"})

    ExGram.Test.stub(:send_message, fn body ->
      {:ok, %{message_id: 1, chat_id: body["chat_id"], text: "ok"}}
    end)

# `stub_error`

Stub an error response for a specific action.

## Example

    error = %ExGram.Error{code: 400, message: "Bad Request"}
    ExGram.Test.stub_error(:send_message, error)

# `unique_name_from_context`

# `verify!`

Verify that all expectations have been met and no unexpected calls were made.

Raises an `ExUnit.AssertionError` if:
- Any expectations remain unfulfilled
- Any unexpected calls were made (calls without a stub or expectation)

## Example

    ExGram.Test.expect(:send_message, %{message_id: 1, text: "ok"})
    ExGram.send_message(123, "Hello")
    ExGram.Test.verify!()  # Passes

# `verify!`

Verify expectations for a specific process.

## Example

    ExGram.Test.verify!(pid)

# `verify_on_exit!`

Register an ExUnit callback that automatically verifies expectations on test exit.

Use this in your test setup for automatic verification:

## Example

    defmodule MyBotTest do
      use ExUnit.Case, async: true
      import ExGram.Test, only: [verify_on_exit!: 1]

      setup :verify_on_exit!

      test "my test" do
        ExGram.Test.expect(:send_message, %{message_id: 1})
        # Test code...
      end
    end

---

*Consult [api-reference.md](api-reference.md) for complete listing*
