# Testing

[< Getting Started](getting-started.md) | [Up: README](../README.md) | [Repo >](repo.md)

HexPort's testing system is built on
[NimbleOwnership](https://hex.pm/packages/nimble_ownership) — the same
ownership library that Mox uses internally. Each test process gets its
own handlers, state, and logs, so `async: true` works out of the box.

## Setup

Start the ownership server once in `test/test_helper.exs`:

```elixir
{:ok, _} = HexPort.Testing.start()
```

This starts a `NimbleOwnership` GenServer. In production, this server
doesn't exist, so the dispatch lookup is zero-cost (a single
`GenServer.whereis` returning `nil`).

## Handler modes

HexPort provides three ways to register test handlers. All are
process-scoped and isolated between concurrent tests.

### Module handler

Register any module that implements the contract's `@behaviour`:

```elixir
HexPort.Testing.set_handler(MyApp.Todos, MyApp.Todos.Fake)
```

Dispatch calls `apply(MyApp.Todos.Fake, operation, args)`.

### Function handler

Register a 2-arity closure `(operation, args) -> result` with pattern
matching:

```elixir
HexPort.Testing.set_fn_handler(MyApp.Todos, fn
  :create_todo, [params] -> {:ok, struct!(Todo, params)}
  :get_todo, [id] -> {:ok, %Todo{id: id, title: "Test"}}
  :list_todos, [_tenant] -> [%Todo{id: "1", title: "Test"}]
end)
```

This is the most common mode for simple tests where you just need
canned return values.

### Stateful handler

Register a 3-arity closure `(operation, args, state) -> {result, new_state}`
with an initial state value:

```elixir
HexPort.Testing.set_stateful_handler(
  MyApp.Todos,
  fn
    :create_todo, [params], todos ->
      todo = struct!(Todo, Map.put(params, :id, map_size(todos) + 1))
      {{:ok, todo}, Map.put(todos, todo.id, todo)}

    :get_todo, [id], todos ->
      case Map.get(todos, id) do
        nil -> {{:error, :not_found}, todos}
        todo -> {{:ok, todo}, todos}
      end

    :list_todos, [_tenant], todos ->
      {Map.values(todos), todos}
  end,
  %{}  # initial state
)
```

State is stored in NimbleOwnership and updated atomically on each
dispatch. This gives you a lightweight in-memory store for tests that
need read-after-write consistency.

For Ecto Repo operations specifically, HexPort ships ready-made
stateful test doubles — see [Repo](repo.md).

## Dispatch logging

Record every call that crosses a port boundary, then assert on the
sequence:

```elixir
setup do
  HexPort.Testing.enable_log(MyApp.Todos)
  HexPort.Testing.set_fn_handler(MyApp.Todos, fn
    :get_todo, [id] -> {:ok, %Todo{id: id}}
  end)
  :ok
end

test "logs dispatch calls" do
  MyApp.Todos.get_todo("42")

  assert [{:get_todo, ["42"], {:ok, %Todo{id: "42"}}}] =
    HexPort.Testing.get_log(MyApp.Todos)
end
```

The log captures `{operation, args, result}` tuples in dispatch order.
Enable logging before making calls; `get_log/1` returns the full
sequence.

## Process sharing and async safety

All test handlers are process-scoped. `async: true` tests run in full
isolation — each test process has its own handlers, state, and logs.

**Task.async children** automatically inherit their parent's handlers
via the `$callers` chain. No setup needed.

**Other processes** (plain `spawn`, Agent, GenServer) need explicit
sharing:

```elixir
HexPort.Testing.allow(MyApp.Todos, self(), agent_pid)
```

`allow/3` also accepts a lazy pid function for processes that don't
exist yet at setup time:

```elixir
HexPort.Testing.allow(MyApp.Todos, self(), fn -> GenServer.whereis(MyWorker) end)
```

## Cleanup

Call `reset/0` to clear all handlers, state, and logs for the current
process:

```elixir
setup do
  HexPort.Testing.reset()
  # ... set up fresh handlers ...
end
```

In practice, most tests just set handlers in `setup` without calling
`reset` — NimbleOwnership's per-process isolation means there's no
cross-test leakage.

## Mox compatibility

Because `defport` generates standard `@callback` declarations, the
contract module works as a Mox behaviour out of the box:

```elixir
# test/support/mocks.ex
Mox.defmock(MyApp.Todos.Mock, for: MyApp.Todos)

# config/test.exs
config :my_app, MyApp.Todos, impl: MyApp.Todos.Mock
```

```elixir
import Mox

setup :verify_on_exit!

test "get_todo returns the expected todo" do
  MyApp.Todos.Mock
  |> expect(:get_todo, fn "42" -> {:ok, %Todo{id: "42"}} end)

  assert {:ok, %Todo{id: "42"}} = MyApp.Todos.get_todo("42")
end
```

This works because HexPort's dispatch resolution checks test handlers
first, then falls back to application config. When using Mox, the
config points to the mock module, and Mox's own process-scoped
expectations provide the isolation.

You can use either approach — HexPort's built-in handlers or Mox —
depending on your preference. HexPort's handlers don't require
defining mock modules or changing config, and the stateful handler
mode has no Mox equivalent.

---

[< Getting Started](getting-started.md) | [Up: README](../README.md) | [Repo >](repo.md)
