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
endrun/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
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).
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-eventts), 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'sstate.valuesmap),: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'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.