How should business logic be tested when using decorators?
See original GitHub issueThe approach of using decorators makes it much easier to get started. We have built an app using the examples as a reference implementation. However, we have hit issues with figuring out how to test the business logic wrapped by decorators.
E.g. for AWS Lambda, the example approach is roughly the following:
from slack_bolt import App
app = App(token="xoxb-token", secret="secret")
@app.event({"type": "message", "subtype": None})
def process_message(message, say, request):
# some more code goes here
return do_some_logic_based_on_message
def handler(event, context):
slack_handler = SlackRequestHandler(app=app)
return slack_handler.handle(event, context)
Usually, the problem of isolating external dependencies for the purpose of unit testing can be resolved by splitting the business logic out into a separate class and injecting the Slack client into it, then using mocks for testing like it’s done in the tests. However, since the decorators get applied at module import, and in this case they refer to instance methods on the app
object rather than functions in the slack_bolt.app
module, the app
object needs to be instantiated as a module variable before the process_message
function definition.
This makes it difficult to set up a BusinessLogic class and pass a reference to app
in the constructor because the methods on the class would be defined before app
can be injected into the object instance.
Another option would be to create an instance of a BusinessLogic class after app
has been created, then decorate static functions that call methods of that class. The reference to app
would have to be passed into the object to allow it to post messages outside of the usual response - e.g. post to channels, etc. For example, like this:
from slack_bolt import App
app = App(token="xoxb-token", secret="secret")
business_logic = BusinessLogic(app)
@app.event({"type": "message", "subtype": None})
def process_message(message, say, request):
return business_logic.do_some_logic_based_on_message(message)
def handler(event, context):
slack_handler = SlackRequestHandler(app=app)
return slack_handler.handle(event, context)
Finally, there is always the option of just calling the bare app.* method without the decorators, which avoids some of the extra lines that get added by the decorators:
from slack_bolt import App
app = App(token="xoxb-token", secret="secret")
business_logic = BusinessLogic(app)
# Register handler directly
app.event({"type": "message", "subtype": None}, business_logic.do_some_logic_based_on_message)
def handler(event, context):
slack_handler = SlackRequestHandler(app=app)
return slack_handler.handle(event, context)
I realise this is a bit rambley, and all of these options seem inelegant and clumsy, so I was wondering if there were some way of testing business logic that I am not seeing.
Issue Analytics
- State:
- Created 3 years ago
- Comments:5 (3 by maintainers)
@latacora-tomekr The main reason why this project’s unit tests have the mock HTTP server is that we want to verify the library’s behavior by performing real HTTP requests, not with only mock objects.
If you would like to have a mock object for
WebClient
in your tests, you can pass the mocked instance to theApp
constructor instead. With this approach, you can use any mock library for it. If this approach does not work for you, you can reuse the mock HTTP server in this project for now.In the long run, we are planning to provide testing tools for Bolt apps (we cannot tell when we can release it, though). If you find obstacles and/or get insights through your Slack app development, sharing your knowledge (in a new issue) would be greatly appreciated.
👋 Let us know if you need further help here. I will close this issue after waiting for your response for a few more days.