Unittesting cherrypy webapp
Asked Answered
A

3

12

I recently had to rewrite our rest api, and made the switch from Flask to Cherrypy (mostly due to Python 3 compatibility). But now I'm stuck trying to write my unit tests, Flask has a really nifty built-in test client, that you can use to sent fake requests to your application (without starting a server.) I can't find any similar functionality for Cherrypy, is there such functionality, or am I stuck starting a server and doing actual requests against it?

Ardyth answered 10/1, 2013 at 14:25 Comment(0)
M
19

As far as I know, CherryPy doesn't indeed provide a facility for this type of testing (no running server). But it's fairly easy to do it nonetheless (though it relies on some of the internals of CherryPy).

Here's a simple showcase:

from StringIO import StringIO
import unittest
import urllib

import cherrypy

local = cherrypy.lib.httputil.Host('127.0.0.1', 50000, "")
remote = cherrypy.lib.httputil.Host('127.0.0.1', 50001, "")

class Root(object):
    @cherrypy.expose
    def index(self):
        return "hello world"

    @cherrypy.expose
    def echo(self, msg):
        return msg

def setUpModule():
    cherrypy.config.update({'environment': "test_suite"})

    # prevent the HTTP server from ever starting
    cherrypy.server.unsubscribe()

    cherrypy.tree.mount(Root(), '/')
    cherrypy.engine.start()
setup_module = setUpModule

def tearDownModule():
    cherrypy.engine.exit()
teardown_module = tearDownModule

class BaseCherryPyTestCase(unittest.TestCase):
    def webapp_request(self, path='/', method='GET', **kwargs):
        headers = [('Host', '127.0.0.1')]
        qs = fd = None

        if method in ['POST', 'PUT']:
            qs = urllib.urlencode(kwargs)
            headers.append(('content-type', 'application/x-www-form-urlencoded'))
            headers.append(('content-length', '%d' % len(qs)))
            fd = StringIO(qs)
            qs = None
        elif kwargs:
            qs = urllib.urlencode(kwargs)

        # Get our application and run the request against it
        app = cherrypy.tree.apps['']
        # Let's fake the local and remote addresses
        # Let's also use a non-secure scheme: 'http'
        request, response = app.get_serving(local, remote, 'http', 'HTTP/1.1')
        try:
            response = request.run(method, path, qs, 'HTTP/1.1', headers, fd)
        finally:
            if fd:
                fd.close()
                fd = None

        if response.output_status.startswith('500'):
            print response.body
            raise AssertionError("Unexpected error")

        # collapse the response into a bytestring
        response.collapse_body()
        return response

class TestCherryPyApp(BaseCherryPyTestCase):
    def test_index(self):
        response = self.webapp_request('/')
        self.assertEqual(response.output_status, '200 OK')
        # response body is wrapped into a list internally by CherryPy
        self.assertEqual(response.body, ['hello world'])

    def test_echo(self):
        response = self.webapp_request('/echo', msg="hey there")
        self.assertEqual(response.output_status, '200 OK')
        self.assertEqual(response.body, ["hey there"])

        response = self.webapp_request('/echo', method='POST', msg="hey there")
        self.assertEqual(response.output_status, '200 OK')
        self.assertEqual(response.body, ["hey there"])

if __name__ == '__main__':
    unittest.main()

Edit, I've extended this answer as a CherryPy recipe.

Mangonel answered 10/1, 2013 at 15:13 Comment(5)
Thanks, going to give this a try today.Ardyth
@Sylvain: is anything special needed for @cherrypy.tools.json_in() support? cherrypy.request.json in testcases causes AttributeError: 'Request' object has no attribute 'json'Emplace
Enabling the tool should be enough but, maybe not via the decorator. Did you try through the config dict instead?Mangonel
@SylvainHellegouarch Would be really nice to have a json example in your fixtures (both in and out). This is very much relevant still, and cherrypy should officially support this :)Emplace
@SylvainHellegouarch Can you update your link to your CherryPy recipes repo? ThxFeliciafeliciano
P
2

It seems that there is an alternate way to perform unittest. I just found and check the following recipe which works fine with cherrypy 3.5.

http://docs.cherrypy.org/en/latest/advanced.html#testing-your-application

    import cherrypy

    from cherrypy.test import helper

    class SimpleCPTest(helper.CPWebCase):
        def setup_server():
            class Root(object):
                @cherrypy.expose
                def echo(self, message):
                    return message

            cherrypy.tree.mount(Root())
        setup_server = staticmethod(setup_server)

        def test_message_should_be_returned_as_is(self):
            self.getPage("/echo?message=Hello%20world")
            self.assertStatus('200 OK')
            self.assertHeader('Content-Type', 'text/html;charset=utf-8')
            self.assertBody('Hello world')

        def test_non_utf8_message_will_fail(self):
            """
            CherryPy defaults to decode the query-string
            using UTF-8, trying to send a query-string with
            a different encoding will raise a 404 since
            it considers it's a different URL.
            """
            self.getPage("/echo?message=A+bient%F4t",
                         headers=[
                             ('Accept-Charset', 'ISO-8859-1,utf-8'),
                             ('Content-Type', 'text/html;charset=ISO-8859-1')
                         ]
            )
            self.assertStatus('404 Not Found')
Plaudit answered 22/11, 2015 at 11:23 Comment(1)
This is certainly how CherryPy tests are written internally. But that means starting up an actual server. That's automatic but my recipe tried to show you could test your handlers without a running server. Your response is not incorrect just a little more on the integration side I'd say.Mangonel
C
0

I found the answer from Sylvain Hellegouarch to be super helpful in figuring this out, but it uses Python 2. I adapted their answer to use Python 3:

import io
import unittest
import urllib
import urllib.parse

import cherrypy
from cherrypy.lib import httputil

local = httputil.Host('127.0.0.1', 50000, '')
remote = httputil.Host('127.0.0.1', 50001, '')

class Root(object):
    @cherrypy.expose
    def index(self):
        return 'hello world'

    @cherrypy.expose
    def echo(self, msg):
        return msg

def setUpModule():
    cherrypy.config.update({'environment': 'test_suite'})

    # prevent the HTTP server from ever starting
    cherrypy.server.unsubscribe()

    cherrypy.tree.mount(Root(), '/')
    cherrypy.engine.start()

setup_module = setUpModule

def tearDownModule():
    cherrypy.engine.exit()

teardown_module = tearDownModule

class BaseCherryPyTestCase(unittest.TestCase):
    def webapp_request(self, path='/', method='GET', **kwargs):
        headers = [('Host', '127.0.0.1')]
        qs = fd = None

        if method in ['POST', 'PUT']:
            qs = urllib.parse.urlencode(kwargs)
            headers.append(('content-type', 'application/x-www-form-urlencoded'))
            headers.append(('content-length', f'{len(qs)}'))
            fd = io.BytesIO(qs.encode())
            qs = None
        elif kwargs:
            qs = urllib.parse.urlencode(kwargs)

        # Get our application and run the request against it
        app = cherrypy.tree.apps['']
        # Let's fake the local and remote addresses
        # Let's also use a non-secure scheme: 'http'
        request, response = app.get_serving(local, remote, 'http', 'HTTP/1.1')
        try:
            response = request.run(method, path, qs, 'HTTP/1.1', headers, fd)
        finally:
            if fd:
                fd.close()
                fd = None

        if response.output_status.startswith(b'500'):
            print(response.body)
            raise AssertionError('Unexpected error')

        # collapse the response into a bytestring
        response.collapse_body()
        return response

class TestCherryPyApp(BaseCherryPyTestCase):
    def test_index(self):
        response = self.webapp_request('/')
        self.assertEqual(response.output_status, b'200 OK')
        # response body is wrapped into a list internally by CherryPy
        self.assertEqual(response.body, [b'hello world'])

    def test_echo(self):
        response = self.webapp_request('/echo', msg='hey there')
        self.assertEqual(response.output_status, b'200 OK')
        self.assertEqual(response.body, [b'hey there'])

        response = self.webapp_request('/echo', method='POST', msg='hey there')
        self.assertEqual(response.output_status, b'200 OK')
        self.assertEqual(response.body, [b'hey there'])
Countercurrent answered 1/4, 2020 at 1:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.