Skip to content

Testing

J. R. Smith edited this page Apr 6, 2024 · 7 revisions

Documentation for writing and executing tests for this project

Background

In order to start up the test Flask server, we require the setup of these components:

  • Flask App object
  • Config object, responsible for reading environment variables
  • RedisClient object, wrapping the FlaskRedis object that provides our connection to a Redis instance
  • Messages object, responsible for handling message strings used within the application
  • Bolt object, which is used for communicating with Slack

Defining Environment Variables Per Test Case

As the application is dependent on defined environment variables, being able to set them to suit a test case enables a wider scope of testing. To set variables, we use the monkeypatch PyTest Fixture.

import pytest

@pytest.fixture
def custom_fixture(monkeypatch):
    monkeypatch.setenv('REDIS_TOKEN', 'REDIS_TOKEN')
    monkeypatch.setenv('REDIS_URL', 'redis://REDIS_URL')
    monkeypatch.setenv('SLACK_BOT_TOKEN', 'BOT_TOKEN')
    monkeypatch.setenv('SLACK_SIGNING_SECRET', 'SLACK_SECRET')
    monkeypatch.setenv('SLACK_SCOPES', '["SCOPE_ONE", "SCOPE_2"]')
    monkeypatch.setenv('DEBUG', 'False')
    monkeypatch.setenv('PORT', '3000')

In the example above, we have set the variables referenced by the Config object. If the monkey-patched variables are assigned before the Config class is imported, then the assigned values will be used when the module does get imported. If not done in the right order, then it will attempt to find those variables in your host Operating System, and most likely fail to start.

For reference, see v8.0.X Section on 'Monkeypatching environment variables'.

Mocking the Redis Database

As we will be building the Archbishop blueprint manually, we will be creating the RedisClient that's passed into the construct_blueprint method manually, too. Since we have access to the client attribute for the RedisClient, we can replace the FlaskRedis/UpstashRedis object with a FakeRedis object.

from fakeredis import FakeRedis
from flask import Flask

from src.app.util.redis import RedisClient

def some_test_function():
    
    app = Flask(__name__)
    
    # Assumed that Environment Variables are defined already
    from src.app.config.config import Config
    config = Config()
    app.config.from_object(config)
    app.app_context().push()
    
    fake_redis = FakeRedis()
    redis_client = RedisClient(app, config.REDIS_URL, config.REDIS_TOKEN)
    redis_client.client = fake_redis
    
    # Now the get, get_complex, set and set_complex methods will use FakeRedis instead of trying to connect to a real database

With this solution, we are able to set the state of the database ahead of the test, then evaluate and make assertions for afterward, while enabling the application to write to the 'database' organically.

Redirecting Calls from the Bolt Object to Slack's API

Currently, we don't have a working solution for 'sandboxing' the Bolt object that acts as an ambassador between our application and Slack's API, though there is an idea that's currently being worked on. First, here's what we've discovered about how the Bolt library works:

Internally, the Bolt library uses urllib to send requests to their API at www.slack.com/api/. This is used inside the WebClient class (client.py), which extends from BaseClient (base_client.py), where most (if not all) calls to the API originate from. It is for this reason that we are investigating the possibility that we can use patch from unittest.mock to intercept outbound requests by patching in a mock for the import of urllib in base_client.py.

All going as expected, we would be able to prevent our application from successfully communicating with Slack's API, and have greater control over how the 'API' would respond, depending on how calls to urllib would be handled. One idea for this is to replicate a 'Stub' in a similar fashion to how you can use Wiremock in JUnit testing: https://wiremock.org/docs/junit-extensions/

Code Coverage

  • Note: it takes a while (10+ mins) for the code coverage badge to be updated in the README.md
  • Each python directory needs an __init__.py to be be treated as a sub-module and be picked up in the code coverage scan

Clone this wiki locally