Middleware

Cannula provides a handful of middleware resolvers with aid in the development process. These middleware resolvers intercept the default resolvers and alter the request/response to provide useful information or completely replace the default resolvers.

Debug Middleware

Use this middleware to log details about the queries that you are running.

By default this will use logging.debug and will use the cannula logger. You can override that when you setup the middleware.

Example with Cannula API

import cannula
from cannula.middleware import DebugMiddleware

api = cannula.API(
    __name__,
    schema=SCHEMA,
    middleware=[
        DebugMiddleware(),
    ],
)

Example with graphql-core-next

You can optionally use this middleware as a standalone with the graphql-core-next:

from cannula.middleware import DebugMiddleware
from graphql import graphql

graphql(
    schema=SCHEMA,
    query=QUERY,
    middleware=[
        DebugMiddleware(),
    ],
)

MockMiddleware

Wrapps an existing schema and resolvers to provide an easy way to automatically mock the responses from Queries, Mutations or Subscriptions. You can choose to mock all resolvers or you could selectively provide mock objects to replace only a handful of calls while preserving the existing resolvers.

This is really useful for end to end testing, as you can simply add a header to the request with the mock objects you wish to replace. This way the entire stack is validated down to the resolver level.

Example Usage

You can add the middleware in a couple different ways. If you are using the cannula API you can add this in the middleware list in the contructor:

import cannula
from cannula.middleware import MockMiddleware

api = cannula.API(
    __name__,
    SCHEMA,
    middleware = [
        MockMiddleware(
            mock_all=True,
            mock_objects={
                'Query': {
                    'field_to_fake': 'mock_value'
                },
                'String': 'I will be used for any String type',
            }
        ),
    ]
)

Or you can use this with just the graphql-core-next library like:

from cannula.middleware import MockMiddleware
from graphql import graphql

graphql(
    schema=SCHEMA,
    query=QUERY,
    middleware=[
        MockMiddleware(
            mock_all=True,
            mock_objects={
                'String': 'just like before'
            }
        ),
    ],
)

Example Using X-Mock-Objects Header

Most testing frameworks have a way to add headers to requests that you are testing. Usually this done for authentication, but we are going to abuse this functionality to tell the server what data to respond with. Here is an example using Cypress.io:

var resourceMock = JSON.stringify({
    "Resource": {
        "name": "Mocky",
        "id": "12345"
    }
});

describe('Test Resource View', function() {
    it('Renders the resource correctly', function() {
        cy.server({
            onAnyRequest: function(route, proxy) {
                proxy.xhr.setRequestHeader(
                    'X-Mock-Objects', resourceMock
                );
            }
        });
        cy.visit('http://localhost:8000/resource/view/');
        cy.get('.resource').within(() => {
            cy.get('.name').should('equal', 'Mocky');
            cy.get('.id').should('equal', '12345');
        });
    })
});

The key difference here from mocking the request is that we are actually making the request to the server at localhost and we know that the routes are setup correctly. We can freely change the payload and the urls that the actual code uses and this test will continue to function. That is, unless we break the contract of this page and those routes no longer respond with the data we are testing for. Since we are not actually mocking the request or the response breaking changes will be realized by this test.

Another reason why this pattern is great is that we are not testing against a mock server that is specifically setup to respond to our request. While that will work just fine the mocks are hidden from the tests in some other file. Changing that file is complicated especially if it is used in multiple tests.

Mock Middleware API

class cannula.middleware.mocks.MockMiddleware(mock_objects={}, mock_all=True, mock_object_header='X-Mock-Objects')

Mocking Middleware

Initialize the MockMiddleware, you can choose to mock all types or just the ones provided by the mock_objects param. Alternatively you can provide the mock_objects via a request header.

Parameters
  • mock_objects (Dict[str, Union[Callable, str, int, float, bool, dict, list]]) – Mapping of return values for types.

  • mock_all (bool) – Whether to mock all resolvers.

  • mock_object_header (str) – Customize the header used to set mock_objects.

class cannula.middleware.mocks.MockObjectStore(mock_objects)

Stores and processes the mock objects to return correct results.

The mock objects can be various types and instead of just returning the raw mock we first want to convert it to a better format. IE if the mock is callable we want to call it first and if the results or the mock are a dict, we need to wrap that in our SuperDict object.

class cannula.middleware.mocks.SuperDict(mapping)

Wraps a dictionary to provide attribute access.

This is returned instead of a plain dict object to handle cases where the underlining resolvers where expecting an object returned. Since we allow setting up mocks via a JSON header this makes it possible to mock only the responses while preserving the rest of the resolvers attached to the schema.

>>> mock = SuperDict({'foo': {'bar': {'baz': 42}}})
>>> print(f"{mock.foo.bar.baz} == {mock['foo']['bar']['baz']}")
42 == 42