bolt-python icon indicating copy to clipboard operation
bolt-python copied to clipboard

Document about the ways to write unit tests for Bolt apps

Open offbyone opened this issue 3 years ago • 12 comments

I am looking to write unit tests for my Bolt API, which consists of an app with several handlers for events / messages. The handlers are both decorated using the app.event decorator, and make use of the app object to access things like the db connection that has been put on it. For example:

# in main.py
app = App(
    token=APP_TOKEN,
    signing_secret=SIGNING_SECRET,
    process_before_response=True,
    token_verification_enabled=token_verification_enabled,
)
app.db = db

# in api.py:
from .main import app

@app.command("/slashcommand")
def slash_command(ack, respond):
    ack()
    results = app.db.do_query()
    respond(...)

The thing is, I cannot find any framework pointers, or documentation, on how I would write reasonable unit tests for this. Presumably ones where the kwargs like ack and respond are replaced by test doubles. How do I do this?

The page URLs

  • https://slack.dev/bolt-python/

Requirements

Please read the Contributing guidelines and Code of Conduct before creating this issue or pull request. By submitting, you are agreeing to those rules.

offbyone avatar Jun 15 '21 21:06 offbyone

Hi @offbyone, thanks for taking the time to write in!

As we have been receiving feedback about testing, it's on our radar. I cannot tell when we can release built-in test support but I myself will be working on it in the latter half of this year.

At this moment, the only thing I can suggest is to do something similar with the tests this project has. You can find so many tests under this package: https://github.com/slackapi/bolt-python/tree/v1.6.1/tests/scenario_tests

The key points are:

  • Have your own mock server like this, which responds to Web API calls / response_url calls from test code. You don't need to use the standard HTTP server module for it.
  • App instances in tests need to have client argument in its constructor. The client's base_url needs to point the mock server's URL this way
  • Prepare request payload example data like this

In addition to above,

I cannot find any framework pointers

You can use any testing frameworks (my recommendation is pytest). The forthcoming testing support won't require your tests to rely on a specific test framework. We may provde some additional utility for pytest etc. for convenience, though.

Presumably ones where the kwargs like ack and respond are replaced by test doubles. How do I do this?

For respond, you can have a local mock HTTP server for handling the request. Regarding the ack call, it will be eventually converted to either an HTTP response or WebSocket message (Socket Mode) but before that, you can verify the value by checking response: Response returned by App#dispatch method as this test does.

I know that this approach is not so easy but it should be reasonable enough. Please consider going with it for now.

I hope this was helpful to you. If everything is clear for now, would you mind closing this issue? For the testing support task, I will come up with a bit more detailed issue soon. Also, for better documentation, we will add the section for testing once we release built-in testing support.

seratch avatar Jun 15 '21 22:06 seratch

Yeah, it ... helps. I guess I'd consider, if I were you, leaving this issue open to track that there is a gap, and resolving it when that documentation exists. If you don't track issues that way, please feel free to close it, but as far as I'm concerned this is still a bug until I can go to your documentation site and see instructions on how to test my bot :D

offbyone avatar Jun 15 '21 23:06 offbyone

if I were you, leaving this issue open to track that there is a gap, and resolving it when that documentation exists

Yeah, I understand this way. It's also fine to keep this open for now. I have a link to this in the new issue that organizes the tasks/discussions around testing support. Thanks for the nice suggestion.

seratch avatar Jun 16 '21 00:06 seratch

I have a further question: Since this kind of testing requires constructing the app object differently, but the object is created at the module level, does someone there have a working pattern for how to do this in the bot code itself? Your example tests all create the app in the tests, but in a real Bolt application by the time you've imported the code, the App already exists and has been configured. So it's really tricky trying to sort out the right method here.

offbyone avatar Jun 30 '21 22:06 offbyone

Hey @seratch is there any update on when this will be released?

carlos-alberto avatar Feb 08 '22 18:02 carlos-alberto

@carlos-alberto I did a quick prototyping late last year. After that, we haven't had good progress due to other priorities. But we're aiming to provide a better solution sometime this year!

seratch avatar Feb 08 '22 21:02 seratch

If it helps those who stumble on this in the future, this is an open-source Bolt app with a moderately sized test suite as an example to dig through.

We've taken the approach of keeping our core functionality in a separate module that doesn't depend on slack_bolt, letting us easily run tests without dealing with bolt trying to start a server or anything like that.

Then in app.py you can just import the module and use the Slack @decorated functions essentially as wrappers over your unit-testable business logic.

This only helps for unit tests, unfortunately haven't delved into integration tests, so unfortunately no review/feedback on seratch's suggestion.

I-Dont-Remember avatar Dec 05 '22 22:12 I-Dont-Remember

Any progress on this since the last update?

jimenezj8 avatar Jul 17 '23 23:07 jimenezj8

Hey @jimenezj8, this is still on my radar and actually I did quick prototyping last year. However, I cannot tell when we can deliver a stable solution due to our bandwidth and priorities. We will update you when we come up with something in the future.

seratch avatar Jul 18 '23 07:07 seratch

Thanks for the update @seratch :slightly_smiling_face: will keep my eyes peeled for another

jimenezj8 avatar Jul 19 '23 02:07 jimenezj8

Hi i was looking into how to write unit tests for our slack bot and found this wonderful library. https://github.com/gabrielfalcao/HTTPretty That make's it easy to mock http requests.

Just be aware that the 1.1 version is broken if you want to use it use 1.0.5. https://github.com/gabrielfalcao/HTTPretty/issues/425

fr12k avatar Sep 14 '23 13:09 fr12k

Came across this as well and figured I'd try to contribute a bit. Here's some modified code from my current app, hopefully it can help someone else. Normally I'd have all of the payload stuff in my conftest.py file to keep things clean, but for the purposes of this I figured I'd put it all in one place.

import pytest
from unittest.mock import AsyncMock
from my.production.code import easter_egg

# This is the shortcut we expect to receive from Slack (well, a heavily truncated one)
mock_shortcut = {
        "message": {
            "text": "Hello, World!",
            "ts": "1234567890.123456"
        },
        "user": {
            "id": "U123456",
            "name": "John Doe"
        },
        "channel": {
            "name": "general"
        },
        "trigger_id": "trigger_12345"
    }

# Upon receiving this shortcut, we will send a 'view' to our user
# This is what we expect it to look like
expected_modal = {
    "type": "modal",
    "callback_id": "hello-modal",
    "title": {
        "type": "plain_text",
        "text": "Greetings!"
    },
    "submit": {
        "type": "plain_text",
        "text": "Good Bye"
    },
    "blocks":[
        {
          "type": "section",
          "text": {
            "type": "plain_text",
            "text": "Frosty the Snowman",
            "emoji": True
          }
        },
        {
          "type": "video",
          "title": {
            "type": "plain_text",
            "text": "Frosty the Snowman",
            "emoji": True
          },
          "title_url": "https://www.youtube.com/watch?v=RRxQQxiM7AA",
          "description": {
            "type": "plain_text",
            "text": "Frosty the Snowman",
            "emoji": True
          },
          "video_url": "https://www.youtube.com/embed/vjscH2WBWjw?feature=oembed&autoplay=1",
          "alt_text": "Frosty the Snowman",
          "thumbnail_url": "https://i.ytimg.com/vi/bSzBBK8gC6c/hqdefault.jpg",
          "author_name": "",
          "provider_name": "YouTube",
          "provider_icon_url": "https://a.slack-edge.com/80588/img/unfurl_icons/youtube.png"
        }
    ]
}

@pytest.mark.asyncio
async def test_easter_egg():
    ack = AsyncMock()
    client = AsyncMock()
    client.views_open = AsyncMock()

    # Test my actual production function 'easter_egg'
    # This should ack the shortcut and then open a view to the user
    await easter_egg(ack, mock_shortcut, client)
    ack.assert_awaited_once # Make sure it was acknowledged
    client.views_open.assert_awaited_once_with( # Make sure the view we get back is correct
        trigger_id=mock_shortcut["trigger_id"],
        view=expected_modal
    )

ChadDa3mon avatar Nov 02 '23 10:11 ChadDa3mon