# Testing `Twilio.Test` provides process-scoped HTTP stubs via NimbleOwnership, so your tests can run with `async: true` without interference. ## Setup Start the test stub server in your `test/test_helper.exs`: ```elixir Twilio.Test.start() ExUnit.start() ``` ## Stubbing Requests Use `Twilio.Test.stub/1` to define how the HTTP layer responds. The stub function receives `(method, url, headers, body)` and returns a `{status, headers, body}` tuple: ```elixir defmodule MyApp.NotifierTest do use ExUnit.Case, async: true test "sends an SMS" do Twilio.Test.stub(fn _method, _url, _headers, _body -> {201, [], ~s({"sid": "SMxxx", "status": "queued", "body": "Hello!"})} end) client = Twilio.client("ACtest", "token") {:ok, msg} = Twilio.Api.V2010.MessageService.create(client, %{ "To" => "+15551234567", "From" => "+15559876543", "Body" => "Hello!" }) assert msg.sid == "SMxxx" assert msg.status == "queued" end end ``` ## Asserting on Request Parameters The stub receives the full request, so you can assert on the URL, body, headers, or method: ```elixir test "sends correct params" do Twilio.Test.stub(fn method, url, _headers, body -> assert method == "POST" assert url =~ "/Messages.json" params = URI.decode_query(body) assert params["To"] == "+15551234567" assert params["Body"] == "Hello!" {201, [], ~s({"sid": "SMxxx"})} end) client = Twilio.client("ACtest", "token") {:ok, _} = Twilio.Api.V2010.MessageService.create(client, %{ "To" => "+15551234567", "Body" => "Hello!" }) end ``` ## Simulating Errors Return non-200 status codes to test error handling: ```elixir test "handles invalid number" do Twilio.Test.stub(fn _method, _url, _headers, _body -> {400, [], ~s({"code": 21211, "message": "The 'To' number is not a valid phone number.", "status": 400})} end) client = Twilio.client("ACtest", "token") {:error, err} = Twilio.Api.V2010.MessageService.create(client, %{ "To" => "not-a-number", "From" => "+15559876543", "Body" => "Test" }) assert err.code == 21211 assert err.message =~ "not a valid phone number" end ``` ## Testing Pagination Stub multiple pages to test auto-paging behavior: ```elixir test "streams through pages" do call_count = :counters.new(1, [:atomics]) Twilio.Test.stub(fn _method, url, _headers, _body -> count = :counters.get(call_count, 1) :counters.add(call_count, 1, 1) if count == 0 do {200, [], ~s({ "messages": [{"sid": "SM001"}, {"sid": "SM002"}], "meta": { "key": "messages", "next_page_url": "#{url}?Page=1", "page": 0, "page_size": 2 } })} else {200, [], ~s({ "messages": [{"sid": "SM003"}], "meta": { "key": "messages", "next_page_url": null, "page": 1, "page_size": 2 } })} end end) client = Twilio.client("ACtest", "token") items = client |> Twilio.Messaging.V1.MessageService.stream() |> Enum.to_list() assert length(items) == 3 end ``` ## Process Isolation Stubs are scoped to the test process that defines them. This means: - **`async: true` works** — concurrent tests don't interfere with each other - **No shared state** — each test sets up its own stubs independently - **Automatic cleanup** — stubs are removed when the test process exits Under the hood, `Twilio.Test` uses `NimbleOwnership` to associate stubs with the calling process. If your test spawns child processes that make Twilio calls, you can allow them to share the parent's stubs: ```elixir test "works in spawned processes" do Twilio.Test.stub(fn _method, _url, _headers, _body -> {200, [], ~s({"sid": "SMxxx", "status": "sent"})} end) task = Task.async(fn -> Twilio.Test.allow(self()) client = Twilio.client("ACtest", "token") Twilio.Api.V2010.MessageService.fetch(client, "SMxxx") end) assert {:ok, msg} = Task.await(task) assert msg.sid == "SMxxx" end ``` ## Testing Webhooks `Twilio.Webhook` functions are pure — they don't make HTTP calls, so test them directly without stubs: ```elixir test "validates webhook signature" do auth_token = "12345" url = "https://myapp.com/twilio/webhook" params = %{"CallSid" => "CA123", "From" => "+14158675310"} signature = Twilio.Webhook.build_signature(url, params, auth_token) assert Twilio.Webhook.valid?(url, params, signature, auth_token) refute Twilio.Webhook.valid?(url, params, "wrong_signature", auth_token) end ``` ## Testing TwiML TwiML builders are also pure functions — assert on the generated XML: ```elixir test "generates voice response" do xml = Twilio.TwiML.VoiceResponse.new() |> Twilio.TwiML.VoiceResponse.say("Hello!", voice: "alice") |> Twilio.TwiML.VoiceResponse.to_xml() assert xml =~ ~s(Hello!) assert xml =~ "" end ``` ## Tips - **Keep stubs minimal.** Only include the fields your test actually checks. The deserializer handles missing fields gracefully. - **Use `async: true`.** The ownership model is designed for it. - **Don't stub webhooks or TwiML.** They are pure functions that don't make HTTP calls. - **Stub the response format correctly.** Twilio returns JSON with `snake_case` keys (e.g. `"account_sid"`, `"date_created"`).