Slink.Testing (Slink v0.6.0)

Copy Markdown View Source

Unit-test your bot's Slink.handle_event/2 without Slack.

Two pieces: event/2 builds realistic event fixtures (through the same normaliser the Socket Mode transport uses, so shapes can't drift from production), and run/3 executes your handler with every Slack call captured instead of sent:

defmodule MyBotTest do
  # run/3 swaps process-global test seams — keep these tests async: false.
  use ExUnit.Case, async: false
  import Slink.Testing

  test "greets a mention in its thread" do
    run = run(MyBot, event(:app_mention, text: "<@U1BOT> hi", thread_ts: "1.0"))

    assert [{"chat.postMessage", %{text: "hi" <> _, thread_ts: "1.0"}}] = run.calls
  end

  test "a slash command is answered ephemerally" do
    run = run(MyBot, event(:slash_command, command: "/deploy", text: "prod"))

    assert [%{response_type: "ephemeral"}] = run.responses
  end
end

run/3 is fully synchronous: the handler runs in the test process, sends bypass the rate-limit workers, and by the time it returns, run.calls and run.responses hold everything the bot did, in order. A handler that raises bubbles into the test, where you want it.

Failure paths are testable by scripting the fake API with :api:

run = run(MyBot, event(:app_mention), api: fn
  "chat.postMessage", _params -> {:error, "channel_not_found"}
  _method, _params -> {:ok, %{"ok" => true}}
end)

Summary

Functions

A handler context for event, as the dispatcher would build it.

Build a Slink.Event fixture of the given type.

Run module's handler for event, capturing every Slack call.

Functions

context(event, opts \\ [])

A handler context for event, as the dispatcher would build it.

Options: :bot_token (default "xoxb-test"), :bot_user_id (default nil — set it to make mentions_me?/1 live).

event(type, attrs \\ [])

Build a Slink.Event fixture of the given type.

The fixture is assembled as a Socket Mode envelope and normalised by Slink.Event.from_socket_mode/1 — the exact production path. Common attributes (all optional, with sensible defaults): :channel, :user, :ts, :text, :thread_ts, :team_id. Anything under :extra (a map of string keys) is merged into the inner payload for shapes not covered here.

Supported types and their specific attributes:

  • :app_mention, :message:text, :thread_ts, :subtype, :event_id. A "message_changed" / "message_deleted" subtype builds the nested shape Slack really sends (your attrs describe the nested message; the wrapper gets its own edit-event ts), so the accessors behave as they do in production.
  • :reaction_added / :reaction_removed:emoji (default "eyes")
  • :app_home_opened:user
  • :slash_command (or :slash_commands) — :command (default "/slink"), :text, :response_url, :trigger_id
  • :block_actions:action_id (default "button"), :value, :message_ts, :thread_ts, :response_url, :trigger_id
  • :view_submission:callback_id, :values (the modal's state.values map), :trigger_id
  • :shortcut, :message_action:callback_id, :trigger_id
  • :assistant_thread_started, :assistant_thread_context_changed:channel (the assistant DM), :thread_ts, :user

run(module, event, opts \\ [])

Run module's handler for event, capturing every Slack call.

Returns a Slink.Testing.Run — the handler's result plus the calls and responses it produced (including a {:reply, …} return value, which is performed exactly as the dispatcher would — so for a view_submission it is dropped, as production's sync-ack path drops it). Everything is synchronous and in-process; nothing touches the network.

Options: :bot_token / :bot_user_id (see context/2), and :api — a 2-arity function (method, params) deciding what each Web API call returns (default: success shapes), for exercising failure paths. Posts to a response_url reach it as the pseudo-method "response_url".

This works by swapping Slink's process-global test seams for the duration of the call, so tests using run/3 must be async: false.