WhatsApp.Test (WhatsApp SDK v0.1.0)

Copy Markdown View Source

Test helpers for WhatsApp SDK.

Provides process-scoped HTTP stubs via NimbleOwnership for writing async: true tests without hitting the real API.

Setup

# In test/test_helper.exs
WhatsApp.Test.start()
ExUnit.start()

Usage

defmodule MyApp.NotifierTest do
  use ExUnit.Case, async: true

  setup do
    WhatsApp.Test.stub(fn request ->
      case request.url do
        "https://graph.facebook.com/v23.0/" <> _ ->
          %{status: 200, body: ~s({"messages":[{"id":"wamid.123"}]}), headers: []}

        _ ->
          %{status: 404, body: ~s({"error":{"message":"Not found"}}), headers: []}
      end
    end)

    :ok
  end

  test "sends a message" do
    client = WhatsApp.client()
    assert {:ok, _} = MyApp.send_whatsapp(client, "Hello!")
  end
end

How It Works

Each test process registers a stub function via stub/1. The stub is stored as metadata in a NimbleOwnership server, keyed by the :http_stub atom.

When the SDK makes an HTTP request, it calls fetch_fun/0 to check whether a stub exists for the current process (or any process in the $callers chain). If a stub is found, the request is routed through the stub function instead of Finch.

Because stubs are process-scoped, tests using async: true are fully isolated from one another.

Summary

Types

Stub request map passed to the stub function.

Stub response map returned from the stub function.

Functions

Allow another process to use the current process's stub.

Start the test stub ownership server.

Register a stub function for the current test process.

Types

request()

@type request() :: %{
  method: :get | :post | :put | :delete | :patch,
  url: String.t(),
  headers: [{String.t(), String.t()}],
  body: String.t() | nil | {:multipart, list()}
}

Stub request map passed to the stub function.

Fields match the HTTP request sent by the client:

  • :method - HTTP method atom (:get, :post, :put, :delete, :patch)
  • :url - Full request URL string
  • :headers - List of {name, value} header tuples
  • :body - Request body (string, nil, or {:multipart, parts})

response()

@type response() :: %{
  status: non_neg_integer(),
  body: String.t(),
  headers: [{String.t(), String.t()}]
}

Stub response map returned from the stub function.

Fields mirror a standard HTTP response:

  • :status - HTTP status code integer (e.g., 200, 400, 429)
  • :body - Response body as a JSON string
  • :headers - List of {name, value} header tuples (default: [])

Functions

allow(pid)

@spec allow(pid()) :: :ok

Allow another process to use the current process's stub.

Useful when your test spawns a process (like a GenServer) that needs to use the same stub as the test process.

Example

setup do
  WhatsApp.Test.stub(fn _request ->
    %{status: 200, body: "{}", headers: []}
  end)

  {:ok, pid} = MyWorker.start_link()
  WhatsApp.Test.allow(pid)
  %{worker: pid}
end

start()

@spec start() :: {:ok, pid()} | :ignore | {:error, term()}

Start the test stub ownership server.

Call this in test/test_helper.exs before ExUnit.start().

stub(fun)

@spec stub((request() -> response())) :: :ok

Register a stub function for the current test process.

The function receives a request map with:

  • :method - HTTP method atom (:get, :post, etc.)
  • :url - Full request URL string
  • :headers - List of {name, value} header tuples
  • :body - Request body (string, nil, or {:multipart, parts})

It must return a response map with:

  • :status - HTTP status code integer
  • :body - Response body string
  • :headers - List of {name, value} header tuples (default: [])

Example

WhatsApp.Test.stub(fn _request ->
  %{status: 200, body: ~s({"success":true}), headers: []}
end)