Testing

Opsdroid contains tooling for testing which is useful for testing opsdroid itself but also for testing your skills and any Connectors, Parsers and Databases created outside the core project.

Opsdroid tests are run using pytest. Fixtures can be imported into your own projects, and are available by default in opsdroid core tests.

There are also some utilities for mocking our and running tests specific to opsdroid.

Fixtures

… py:function:: opsdroid()

module:

opsdroid.testing

Fixture with a plain instance of opsdroid.

Will yield an instance of :class:opsdroid.core.OpsDroid which hasn’t been loaded.

rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~opsdroid.core.OpsDroid``

… py:function:: mock_api(mock_api_obj)

module:

opsdroid.testing

Fixture for mocking API calls to a web service.

Will yield a running instance of

class:

opsdroid.testing.ExternalAPIMockServer, which has been configured with any routes specified through @pytest.mark.add_response() decorators, or modification of the

func:

~opsdroid.testing.fixtures.mock_api_obj fixture before the test is called.

All arguments and keyword arguments passed to pytest.mark.add_response are passed through to .ExternalAPIMockServer.add_response.

An example test would look like::

@pytest.mark.add_response("/test", "GET")
@pytest.mark.add_response("/test2", "GET", status=500)
@pytest.mark.asyncio
async def test_hello(mock_api):
    async with aiohttp.ClientSession() as session:
        async with session.get(f"{mock_api.base_url}/test") as resp:
            assert resp.status == 200
            assert mock_api.called("/test")

        async with session.get(f"{mock_api.base_url}/test2") as resp:
            assert resp.status == 500
            assert mock_api.called("/test2")
rtype:
sphinx_autodoc_typehints_type:

\:py\:class\:\~opsdroid.testing.external_api.ExternalAPIMockServer``

Utilities

… py:class:: ExternalAPIMockServer()

module:

opsdroid.testing

canonical:

opsdroid.testing.external_api.ExternalAPIMockServer

A webserver which can pretend to be an external API.

The general idea with this class is to allow you to push expected responses onto a stack for each API call you expect your test to make. Then as your tests make those calls each response is popped from the stack.

You can then assert that routes were called and that data and headers were sent correctly.

Your test will need to switch the URL of the API calls, so the thing you are testing should be configurable at runtime. You will also need to capture the responses from the real API your are mocking and store them as JSON files. Then you can push those responses onto the stack at the start of your test.

… rubric:: Examples

A simple example of pushing a response onto a stack and making a request::

import pytest
import aiohttp

from opsdroid.testing import ExternalAPIMockServer

@pytest.mark.asyncio
async def test_example():
    # Construct the mock API server and push on a test method
    mock_api = ExternalAPIMockServer()
    mock_api.add_response("/test", "GET", None, 200)

    # Create a closure function. We will have our mock_api run this concurrently with the
    # web server later.
    async with mock_api.running():
        # Make an HTTP request to our mock_api
        async with aiohttp.ClientSession() as session:
            async with session.get(f"{mock_api.base_url}/test") as resp:

                # Assert that it gives the expected responses
                assert resp.status == 200
                assert mock_api.called("/test")

… py:method:: ExternalAPIMockServer.add_response(route, method, response=None, status=200)

module:

opsdroid.testing

Push a mocked response onto a route.

rtype:

:sphinx_autodoc_typehints_type:\:py\:obj\:\None``

… py:property:: ExternalAPIMockServer.base_url

module:

opsdroid.testing

type:

str

Return the base url of the web server.

… py:method:: ExternalAPIMockServer.call_count(route, method=None)

module:

opsdroid.testing

Route has been called n times.

type route:

:sphinx_autodoc_typehints_type:\:py\:class\:\str``

param route:

The API route that we want to know if was called.

rtype:

… py:function:: run_unit_test(opsdroid, test, *args, start_timeout=1, **kwargs)

module:

opsdroid.testing

async:

Run a unit test function against opsdroid.

This method should be used when testing on a loaded but stopped instance of opsdroid. The instance will be started concurrently with the test runner. The test runner will block until opsdroid is ready and then the test will be called. Once the test has returned opsdroid will be stopped and unloaded.

type opsdroid:

:sphinx_autodoc_typehints_type:\:py\:class\:\~opsdroid.core.OpsDroid``

param opsdroid:

A loaded but stopped instance of opsdroid.

type test:

:sphinx_autodoc_typehints_type:\:py\:class\:\~typing.Awaitable``

param test:

A test to execute concurrently with opsdroid once it has been started.

type start_timeout:
param start_timeout:

Wait up to this timeout for opsdroid to say that it is running.

rtype:

:sphinx_autodoc_typehints_type:\:py\:data\:\~typing.Any``

returns:

… py:function:: call_endpoint(opsdroid, endpoint, method=‘GET’, data_path=None, data=None, headers=None, **kwargs)

module:

opsdroid.testing

async:

Call an opsdroid API endpoint with the provided data.

This method should be used when testing on a running instance of opsdroid. The endpoint will be appended to the base url of the running opsdroid, so you do not need to know the address of the running opsdroid. An HTTP request will be made with the provided method and data or data_path for methods that support it.

For methods like "POST" either data or data_path should be set.

type opsdroid:

:sphinx_autodoc_typehints_type:\:py\:class\:\~opsdroid.core.OpsDroid``

param opsdroid:

A running instance of opsdroid.

type endpoint:

:sphinx_autodoc_typehints_type:\:py\:class\:\str``

param endpoint:

The API route to call.

type method:

:sphinx_autodoc_typehints_type:\:py\:class\:\str``

param method:

The HTTP method to use when calling.

type data_path:

:sphinx_autodoc_typehints_type:\:py\:data\:\~typing.Optional`\ \[:py:class:`str`]`

param data_path: