The Mock provider
ExAtlas.Providers.Mock is an in-memory implementation of the full
ExAtlas.Provider behaviour. It runs synchronously — pods are
:running immediately after spawn_compute/2, jobs complete on the
next get_job/2 call. Perfect for testing your own code that depends
on ExAtlas without hitting any network.
Setup
# test/test_helper.exs
ExUnit.start()
ExAtlas.Providers.Mock.ensure_started()# your test
defmodule MyApp.InferenceTest do
use ExUnit.Case, async: false
setup do
ExAtlas.Providers.Mock.reset()
:ok
end
test "my code handles a running pod" do
{:ok, compute} = MyApp.start_session(user_id: 42)
# my code calls ExAtlas internally; we point it at :mock via config
assert compute.status == :running
assert compute.provider == :mock
end
endSet the default provider per test suite:
# config/test.exs
config :ex_atlas,
default_provider: :mock,
start_orchestrator: falseShared conformance suite
ExAtlas.Test.ProviderConformance is a use macro that runs the same
suite against any provider implementation, so every provider in the
library (and yours) exercises the same contract.
defmodule MyCloud.ProviderTest do
use ExUnit.Case, async: false
use ExAtlas.Test.ProviderConformance,
provider: MyCloud.Provider,
reset: {MyCloud.TestHelpers, :reset_bypass, []}
endThe :reset MFA is called before every test. Use it to:
- Reset Bypass expectations (for HTTP-based providers).
- Clear ETS or Mnesia fixtures.
- Rotate in-memory state.
Integration tests against a live cloud
defmodule ExAtlas.Providers.RunPodLiveTest do
use ExUnit.Case, async: false
@moduletag :live
@tag timeout: 120_000
test "spawn → wait-for-healthy → terminate on community RTX A4000" do
{:ok, compute} =
ExAtlas.spawn_compute(
provider: :runpod,
gpu: :a4000,
image: "ubuntu:22.04",
cloud_type: :community,
ports: [{22, :tcp}]
)
on_exit(fn -> ExAtlas.terminate(compute.id, provider: :runpod) end)
assert compute.status in [:provisioning, :running]
end
endAdd :live to the excluded tags in test_helper.exs:
ExUnit.start(exclude: [:live])Run live tests explicitly:
RUNPOD_API_KEY=sk_... mix test --only live
HTTP-level tests with Bypass
Mimic a provider's response without hitting the network:
setup do
bypass = Bypass.open()
opts = [
provider: :runpod,
api_key: "test-key",
base_url: "http://localhost:#{bypass.port}"
]
{:ok, bypass: bypass, opts: opts}
end
test "spawn_compute POSTs /pods", %{bypass: bypass, opts: opts} do
Bypass.expect_once(bypass, "POST", "/pods", fn conn ->
{:ok, raw, conn} = Plug.Conn.read_body(conn)
body = Jason.decode!(raw)
assert body["gpuTypeIds"] == ["NVIDIA H100 80GB HBM3"]
conn
|> Plug.Conn.put_resp_header("content-type", "application/json")
|> Plug.Conn.resp(
201,
Jason.encode!(%{"id" => "pod_abc", "desiredStatus" => "RUNNING"})
)
end)
{:ok, compute} =
ExAtlas.spawn_compute([gpu: :h100, image: "x"] ++ opts)
assert compute.id == "pod_abc"
endOrchestrator tests
The orchestrator uses Phoenix.PubSub and Registry. Start supervised
children explicitly in setup so each test gets a fresh supervision
tree:
setup do
Application.put_env(:ex_atlas, :start_orchestrator, true)
Application.put_env(:ex_atlas, :default_provider, :mock)
ExAtlas.Providers.Mock.reset()
start_supervised!({Registry, keys: :unique, name: ExAtlas.Orchestrator.ComputeRegistry})
start_supervised!({DynamicSupervisor, name: ExAtlas.Orchestrator.ComputeSupervisor,
strategy: :one_for_one})
start_supervised!({Phoenix.PubSub, name: ExAtlas.PubSub})
:ok
endFollow project conventions:
- Use
Process.monitor/1+assert_receive {:DOWN, ...}to wait for exits. NeverProcess.sleep/1to wait for processes to die. - Use
start_supervised!/1so ExUnit tears processes down between tests. - Set
async: falseon suites that touch global application env.