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.