Sylvain Hellegouarch avatar Sylvain Hellegouarch committed 50aff88

Unit testing CherryPy recipe without its HTTP server running

Comments (0)

Files changed (3)

testing/unit/serverless/app.py

+# -*- coding: utf-8 -*-
+import cherrypy
+
+class Root(object):
+    @cherrypy.expose
+    def index(self):
+        return "hello world"
+
+    @cherrypy.expose
+    def echo(self, msg):
+        return msg

testing/unit/serverless/cptestcase.py

+# -*- coding: utf-8 -*-
+from StringIO import StringIO
+import unittest
+import urllib
+
+import cherrypy
+
+# Not strictly speaking mandatory but just makes sense
+cherrypy.config.update({'environment': "test_suite"})
+
+# This is mandatory so that the HTTP server isn't started
+# if you need to actually start (why would you?), simply
+# subscribe it back.
+cherrypy.server.unsubscribe()
+
+# simulate fake socket address... they are irrelevant in our context
+local = cherrypy.lib.httputil.Host('127.0.0.1', 50000, "")
+remote = cherrypy.lib.httputil.Host('127.0.0.1', 50001, "")
+
+__all__ = ['BaseCherryPyTestCase']
+
+class BaseCherryPyTestCase(unittest.TestCase):
+    def request(self, path='/', method='GET', app_path='', scheme='http',
+                proto='HTTP/1.1', data=None, headers=None, **kwargs):
+        """
+        CherryPy does not have a facility for serverless unit testing.
+        However this recipe demonstrates a way of doing it by
+        calling its internal API to simulate an incoming request.
+        This will exercise the whole stack from there.
+
+        Remember a couple of things:
+
+        * CherryPy is multithreaded. The response you will get
+          from this method is a thread-data object attached to
+          the current thread. Unless you use many threads from
+          within a unit test, you can mostly forget
+          about the thread data aspect of the response.
+
+        * Responses are dispatched to a mounted application's
+          page handler, if found. This is the reason why you
+          must indicate which app you are targetting with
+          this request by specifying its mount point.
+
+        You can simulate various request settings by setting
+        the `headers` parameter to a dictionary of headers,
+        the request's `scheme` or `protocol`.
+
+        .. seealso: http://docs.cherrypy.org/stable/refman/_cprequest.html#cherrypy._cprequest.Response
+        """
+        # This is a required header when running HTTP/1.1
+        h = {'Host': '127.0.0.1'}
+
+        if headers is not None:
+            h.update(headers)
+
+        # If we have a POST/PUT request but no data
+        # we urlencode the named arguments in **kwargs
+        # and set the content-type header
+        if method in ('POST', 'PUT') and not data:
+            data = urllib.urlencode(kwargs)
+            kwargs = None
+            h['content-type'] = 'application/x-www-form-urlencoded'
+
+        # If we did have named arguments, let's
+        # urlencode them and use them as a querystring
+        qs = None
+        if kwargs:
+            qs = urllib.urlencode(kwargs)
+
+        # if we had some data passed as the request entity
+        # let's make sure we have the content-length set
+        fd = None
+        if data is not None:
+            h['content-length'] = '%d' % len(data)
+            fd = StringIO(data)
+
+        # Get our application and run the request against it
+        app = cherrypy.tree.apps.get(app_path)
+        if not app:
+            # XXX: perhaps not the best exception to raise?
+            raise AssertionError("No application mounted at '%s'" % app_path)
+
+        # Cleanup any previous returned response
+        # between calls to this method
+        app.release_serving()
+
+        # Let's fake the local and remote addresses
+        request, response = app.get_serving(local, remote, scheme, proto)
+        try:
+            h = [(k, v) for k, v in h.iteritems()]
+            response = request.run(method, path, qs, proto, h, 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

testing/unit/serverless/test.py

+# -*- coding: utf-8 -*-
+import cherrypy
+
+from app import Root
+from cptestcase import BaseCherryPyTestCase
+
+def setUpModule():
+    cherrypy.tree.mount(Root(), '/')
+    cherrypy.engine.start()
+setup_module = setUpModule
+
+def tearDownModule():
+    cherrypy.engine.exit()
+teardown_module = tearDownModule
+
+class TestCherryPyApp(BaseCherryPyTestCase):
+    def test_index(self):
+        response = self.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.request('/echo', msg="hey there")
+        self.assertEqual(response.output_status, '200 OK')
+        self.assertEqual(response.body, ["hey there"])
+
+        response = self.request('/echo', method='POST', msg="back from the future")
+        self.assertEqual(response.output_status, '200 OK')
+        self.assertEqual(response.body, ["back from the future"])
+
+if __name__ == '__main__':
+    import unittest
+    unittest.main()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.