Skip to content

Add slack_bolt.testing for unit testing Bolt apps - #1615

Open
Mohith26 wants to merge 2 commits into
slackapi:mainfrom
Mohith26:feat/testing-utilities
Open

Mohith26 wants to merge 2 commits into
slackapi:mainfrom
Mohith26:feat/testing-utilities

Conversation

@Mohith26

@Mohith26 Mohith26 commented Nov 4, 2026

Copy link
Copy Markdown

Summary

Adds slack_bolt.testing, built-in support for unit testing Bolt apps without connecting to Slack (#380). It follows the approach Bolt's own tests/scenario_tests use (a local mock server plus signed requests), packaged so app developers don't have to rebuild it. No new dependencies; the server uses the standard library.

  • MockSlackServer: an in-process HTTP server that stands in for the Web API and for response_url. It records every API call (method, parsed args, token) and every message posted to a response_url. auth.test returns a bot installation by default, other methods return {"ok": true}, and set_response() sets canned responses, including ok: false errors that raise SlackApiError like the real API.
  • BoltTestClient(app, server): builds signed requests for events, messages, slash commands, block actions, global and message shortcuts, and view submissions, and sends them through app.dispatch(). It returns a DispatchResult with the ack status/body/json, plus the API calls and respond() bodies that request caused.
  • AsyncBoltTestClient (in slack_bolt.testing.async_client, so sync users don't import aiohttp) does the same for AsyncApp.
  • slack_bolt.testing.payloads: the payload builders on their own, plus to_http_body() and signed_headers().

Example:

with MockSlackServer() as server:
    app = App(client=server.web_client(), signing_secret="test-secret")

    @app.command("/hello")
    def hello(ack, command, respond):
        ack()
        respond(f"Hi <@{command['user_id']}>!")

    result = BoltTestClient(app, server).slash_command("/hello", user_id="U123")
    assert result.status == 200
    assert result.responds == [{"text": "Hi <@U123>!"}]

Requests go through the full middleware chain (request verification, authorization, listener matching). Two design choices worth flagging:

  • To make results deterministic, BoltTestClient runs listeners and lazy listeners inline (an InlineExecutor on the app's listener runner), so side effects are done when the call returns even with process_before_response=False. The async client instead waits for the tasks the dispatch started, with a timeout.
  • It points app.client.base_url at the mock server, so apps created at import time with a real token (and token_verification_enabled=False) can be tested without restructuring.

There is a new docs page, docs/english/concepts/testing.md, linked from the sidebar. I posted the design on #380 first; happy to rename things, trim the scope (for example ship only the payload builders and server first), or move it to a separate package if you'd rather not add public API to slack_bolt.

Testing

  • New tests: tests/slack_bolt/testing/test_testing.py and tests/slack_bolt_async/testing/test_async_testing.py (19 tests): recorded calls and canned responses, error responses, slash command ack + respond(), say() from a message listener, listeners that finish after ack with process_before_response=False, lazy listeners, block actions and views.open, view submission response_action errors, both shortcut types, 404 for unhandled requests and 401 for a bad signature, 500 for a listener error, an app created with a default client, payload encoding and signatures, and the async timeout.
  • pytest tests/slack_bolt tests/slack_bolt_async tests/scenario_tests tests/scenario_tests_async: 849 passed.
  • ./scripts/format.sh --no-install, ./scripts/lint.sh --no-install: clean. mypy slack_bolt/testing: no issues. The full ./scripts/run_mypy.sh only reports errors in adapter modules whose optional dependencies I don't have installed.
  • I did not run ./scripts/install_all_and_run_tests.sh, so the adapter test suites (which need the optional framework dependencies) were not run locally. This change doesn't touch adapters.

I used an AI assistant (Claude) while writing this; I reviewed and tested all of it.

Category

  • slack_bolt.App and/or its core components
  • slack_bolt.async_app.AsyncApp and/or its core components
  • Adapters in slack_bolt.adapter
  • Document pages under /docs
  • Others

Requirements

  • I've read and understood the Contributing Guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've run ./scripts/install_all_and_run_tests.sh after making the changes. (See Testing: core and scenario suites run, adapter suites not run locally.)

@Mohith26
Mohith26 requested review from a team as code owners November 4, 2026 14:34
@salesforce-cla

salesforce-cla Bot commented Nov 4, 2026

Copy link
Copy Markdown

Thanks for the contribution! Before we can merge this, we need @Mohith26 to sign the Salesforce Inc. Contributor License Agreement.

@mwbrooks

mwbrooks commented Nov 4, 2026

Copy link
Copy Markdown
Member

Thanks @Mohith26 for putting this together! 🙇 It's a big addition, so we're discussing it as a team and will follow up here.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants