单元测试CherryPy Web应用程序

12

最近我不得不重写我们的rest api,并从Flask切换到了Cherrypy(主要是因为Python 3兼容性)。但现在我陷入了编写单元测试的困境,Flask有一个非常巧妙的内置测试客户端,可以用于向应用程序发送虚拟请求(而不启动服务器)。我找不到类似于Cherrypy的功能,是否有这样的功能,或者我必须启动服务器并针对其执行实际请求?

3个回答

19
据我所知,CherryPy确实没有提供这种类型测试的工具(没有运行服务器)。不过,仍然很容易做到(尽管它依赖于一些CherryPy内部的机制)。
下面是一个简单的示例:
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()

编辑,我已将此答案扩展为CherryPy配方


谢谢,今天会尝试一下。 - Blubber
@Sylvain:@cherrypy.tools.json_in()支持需要特殊的设置吗?在测试用例中使用cherrypy.request.json会导致AttributeError: 'Request' object has no attribute 'json'错误。 - user1338062
启用该工具应该足够了,但也许不是通过装饰器。你尝试过通过配置字典来实现吗? - Sylvain Hellegouarch
@SylvainHellegouarch,如果你的fixtures(输入和输出)中有一个JSON示例,那就太好了。这仍然非常相关,而且cherrypy应该正式支持它 :) - user1338062
@SylvainHellegouarch,你能更新一下你的CherryPy菜谱库的链接吗?谢谢。 - huyz

2

看起来有一种替代方法可以执行单元测试。 我刚刚发现并检查了以下配方,它在使用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')

这确实是CherryPy测试内部编写的方式。但这意味着启动一个实际的服务器。虽然这是自动的,但我的方法尝试向您展示如何在没有运行服务器的情况下测试处理程序。您的回答并不不正确,只是我认为它更多地涉及集成方面。 - Sylvain Hellegouarch

0

我发现 Sylvain Hellegouarch 的答案对我在解决这个问题时非常有帮助,但是它使用的是 Python 2。我改编了他们的答案来使用 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'])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接