Tutorial

In this tutorial we will walk through the building of a simple StackInABoxService that implements a simple key-value store as a RESTful API. Then we’ll show how to hook it up to a few tests using `requests-mock`.

First Step

Before continuing be sure to install StackInABox in your test environment. In your test module, add a directory to hold your StackInABoxService and cd into it:

$ cd tests
$ mkdir service

Next we’ll create a new file that will host the test StackInABoxService:

$ touch lookupService.py

Open the file using your favorite text editor and add the following:

from stackinabox.services.service import StackInABoxService

class LookupService(StackInABoxService):

    def __init__(self):
        super(LookupService, self).__init__('lookup')
        # We'll store values in self.storage
        self.storage = dict()

This is the basic structure of the service. Everything from here is adding HTTP Resources to the test service. For the key-value store we’ll add a POST, GET, and DELETE. We’ll also add a HEAD to get a count of how many items are in the key-value store.

First, let’s add check to see how many items there are. Add the following method to the LookupService class:

def get_key_value_count(self, request, uri, headers):
    headers['X-Key-Value-Count'] = len(self.storage)
    return (204, headers, '')

This implements the logic that the RESTful API end-point for the HEAD operation. The operation will be generic the whole service; so we can use the very simple registration operation. To do so, update the `__init__(self)` to the following:

def __init__(self):
    super(LookupService, self).__init__('lookup')
    # We'll store values in self.storage
    self.storage = dict()
    self.register(StackInABoxService.GET,
                  '/',
                  LookupService.get_key_value_count)

Please note that the parameter to the `__init__()` is extremely important as this is what identifies the service within StackInABox. We’ll discuss this more later when we hookup the test.

This version of the LookupService can will work in a test now. However, we want to make more useful tests. The key will be used in the URI, and the value will be in the body. So let’s add the other end-points:

def set_key_value(self, request, uri, headers):
    # some test times the body is a string type
    # other times it's a byte type
    key_value = request.body.decode('utf-8') if hasattr(
        request.body, 'decode') else request.body
    key_name = uri[1:]

    self.storage[key_name] = key_value

    return (204, headers, '')

def get_key(self, request, uri, headers):
    key_name = uri[1:]
    if key_name in self.storage:
        key_value = self.storage[key_name]
        return (200, headers, key_value)
    else:
        return (404, headers, 'Not Found')

def delete_key(self, request, uri, headers):
    key_name = uri[1:]
    if key_name in self.storage:
        del self.storage[key_value]
        return (204, headers, '')
    else:
        return (404, headers, 'Not Found')

The above relies on being able to match the URI using a regex pattern such as the following:

^/[0-9a-zA-Z]?$

Fortunately, StackInABox provides the ability to match a handler function via either a static string like we did with the HEAD operation or with a regex like in the following to register the three handler functions above:

import regex

class LookupService(StackInABoxService):

    LookupServiceKeyRegEx = re.compile('^/[0-9a-zA-Z]?$')

    def __init__(self):
        super(LookupService, self).__init__('lookup')
        # We'll store values in self.storage
        self.storage = dict()
        # registration via a static string:
        self.register(StackInABoxService.HEAD,
                      '/',
                      LookupService.get_key_value_count)
        # registration via regexi:
        self.register(StackInABoxService.DELETE,
                      LookupService.LookupServiceKeyRegEx,
                      LookupService.delete_key)
        self.register(StackInABoxService.GET,
                      LookupService.LookupServiceKeyRegEx,
                      LookupService.get_key)
        self.register(StackInABoxService.POST,
                      LookupService.LookupServiceKeyRegEx,
                      LookupService.set_key_value)

So the final class will look like:

import regex

from stackinabox.services.service import StackInABoxService

class LookupService(StackInABoxService):

    LookupServiceKeyRegEx = re.compile('^/[0-9a-zA-Z]?$')

    def __init__(self):
        super(LookupService, self).__init__('lookup')
        # We'll store values in self.storage
        self.storage = dict()
        # registration via a static string:
        self.register(StackInABoxService.HEAD,
                      '/',
                      LookupService.get_key_value_count)
        # registration via regexi:
        self.register(StackInABoxService.DELETE,
                      LookupService.LookupServiceKeyRegEx,
                      LookupService.delete_key)
        self.register(StackInABoxService.GET,
                      LookupService.LookupServiceKeyRegEx,
                      LookupService.get_key)
        self.register(StackInABoxService.POST,
                      LookupService.LookupServiceKeyRegEx,
                      LookupService.set_key_value)

    def get_key_value_count(self, request, uri, headers):
        headers['X-Key-Value-Count'] = len(self.storage)
        return (204, headers, '')

    def set_key_value(self, request, uri, headers):
        # some test times the body is a string type
        # other times it's a byte type
        key_value = request.body.decode('utf-8') if hasattr(
            request.body, 'decode') else request.body
        key_name = uri[1:]

        self.storage[key_name] = key_value

        return (204, headers, '')

    def get_key(self, request, uri, headers):
        key_name = uri[1:]
        if key_name in self.storage:
            key_value = self.storage[key_name]
            return (200, headers, key_value)
        else:
            return (404, headers, 'Not Found')

    def delete_key(self, request, uri, headers):
        key_name = uri[1:]
        if key_name in self.storage:
            del self.storage[key_value]
            return (204, headers, '')
        else:
            return (404, headers, 'Not Found')

Second Step: Hooking up a test

Testing with StackInABox is actually really easy as you don’t have to hook up complicated mockings. For this example we’ll use the `requests-mock`; however, `httpretty` and `responses` are also natively supported.

To start, we need to setup the test structure as follows:

import unittest

import requests

import stackinabox.util.requests_mock
from stackinabox.stack import StackInABox

from tests.service.lookupService import LookupService


class TestLookupService(unittest.TestCase):

    def setUp(self):
        super(TestLookupService, self).setUp()
        # Register LookupService for use in the test
        StackInABox.register_service(LookupService())
        # Access the requests session for use in the test
        self.session = requests.Session()

    def tearDown(self):
        super(TestLookupSerice, self).tearDown()
        # Reset StackInABox for the next test
        StackInABox.reset_services()
        # Reset Requests for the next request
        self.session.close()

The above setups each test and ensure each one is completely separate so they don’t interfere with each other. Now we’ll add a simple test to it:

def test_basic(self):
    stackinabox.util.requests_mock.request_mock_session_registration(
        'localhost', self.session)
    res = self.session.head('http://localhost/lookup/')
    self.assertEqual(res.status_code, 204)
    self.assertIn(res.headers, 'X-Key-Value-Count')
    self.assertEqual(res.headers['X-Key-Value-Count'], '0')

StackInABox provides some utility functions to work with the support testing frameworks. In this instance, we’re going to use the one for `requests-mock` and it’s registration for using the `requests` session object, though you also do the registration without the session object and just use `requests` itself too.

The utility function performs the rest of the setup to use StackInABox, which will be registered under the `http://` and `https:// protocols. The first parameter to the utility function is the base of the URI name for any StackInABox calls. Under this location will be each service based on the name in it’s initialization, f.e lookup. Thus several different services can all be registered at the same time as long as their names do not collide. If a StackInABoxService with the same name is already registered then the registration will throw an exception.

The rest of the test runs just as it would in normal usage for an application; the only difference is that you have to specify the URL for the StackInABox. If you services rely on external systems like the OpenStack Keystone Service that provides a catalog of related-services, then you may need to implement the required services and related-service lookup functionality for all your code to work properly.