Pages

Monday, October 10, 2011

Proxying tests

So I wrote that WSGI app. It's rock solid, I have unit tests, functional tests and good coverage. I'm thrilled! Now it's being deployed to production! Thrilled, but anxious too. Already I'm clicking around, making sure shit actually works. Primitive instinct -- fair enough. The application responds, it seems to work but I know I can't reasonably tests every single feature with mouse clicks.

Fortunately, I always test my code.

For functional tests, I use PasteDeploy to load my Pyramid application and webtest to issue requests and test responses.

from unittest import TestCase
from webtest import TestApp
from os.path import abspath
from paste.deploy import loadapp

# load the WSGI stack
conf_dir = abspath(__file__ + '/../..')
app = loadapp('config:test.ini', relative_to=conf_dir)
app = TestApp(app)


class TestHome(TestCase):

    def test_home(self):
        response = app.get('/', status=200)
        # do assertions ...

webtest.TestApp wraps your WSGI stack to provide convenient handles such as .get() .post() .put() .delete() ... It saves you from making raw WSGI calls like this: app(environ, start_response)

These tests are just plain Python function calls. So how can I run these function calls against my production server? WSGIProxy!

WSGIProxy

Bleh. Proxy. I have never setup a proxy server, don't like proxies, I don't understand them. I'm gonna stop reading now.

First of all, WSGIProxy is NOT about setting up a proxy server. It's a WSGI app which turns your WSGI calls into HTTP requests. Pretty cool. This is how I set it up:

from wsgiproxy.app import WSGIProxyApp
from webtest import TestApp

remote_host = "http://127.0.0.1.6543"
app = WSGIProxyApp(remote_host)
app = TestApp(app, extra_environ={"REMOTE_ADDR": remote_host})


class TestHome(TestCase):

    def test_home(self):
        response = app.get('/', status=200)
        # do assertions ...

Whenever I call app.get('/', status=200) an actual HTTP request is issued to the server. Neat! Now I can run all my functional tests against my server in production with minor code change!

But what if I don't want to change my code back and forth? nose-testconfig!

nose-testconfig

Whether I want to run my functional tests against my WSGI stack or a remote server: I don't want to touch my code!

I want to pass the remote URL on the command line, or fallback on my WSGI stack if no URL was given. But have you tried passing your own arguments to nose?

$ nosetests --url http://example.com
Usage: nosetests [options]

nosetests: error: no such option: --url

You can't.

The library nose-testconfig was specifically written to address this concern. Once installed, you can do:

nosetests --tc=url:http://example.com

This will be mapped into a key/value dictionary in your Python code:

from testconfig import config
assert config["url"] == "http://example.com"  # True

Now I can write:

def get_app():
    from testconfig import config
    remote_host = config.get('url')

    # Extra environ headers to be passed to TestApp
    extra_environ = {}

    if remote_host:
        # Setup a WSGI proxy
        from wsgiproxy.app import WSGIProxyApp
        extra_environ['REMOTE_ADDR'] = remote_host
        app = WSGIProxyApp(remote_host)
    else:
        # Load the local WSGI stack
        from os.path import abspath
        from paste.deploy import loadapp
        conf_dir = abspath(__file__ + '/../..')
        app = loadapp('config:test.ini', relative_to=conf_dir)

    from webtest import TestApp
    app = TestApp(app, extra_environ=extra_environ)
    return app

app = get_app()

class TestHome(TestCase):

    def test_home(self):
        response = app.get('/', status=200)
        # do assertions ...

Now my test infrastructure allows me to test locally as I develop or test remotely once code is deployed, just by passing a URL on the command line.

Final note

Of course, coverage will look broken with WSGIProxy. Moreover, keep in mind that mocking / patching your code might not behave as you would normally expect. If you only want to run a subset of your tests, you may want to use nose's attribute selector plugin to select them.

2 comments:

  1. Thanks for this really useful technique. In case it saves someone else the google, you also have to

    from paste.deploy import loadapp

    ReplyDelete