如何在 Python 3 中对 CherryPy webapp 进行单元测试?

How to unittest a CherryPy webapp in Python 3?

我正在尝试在 CherryPy 中对 Web 应用程序进行单元测试。我发现 this answer 这正是我想要做的,但它是写在 Python 2 中的,我在 Python 3 中工作时遇到了一些问题。这是我修改过的代码:

import unittest
from io import StringIO
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.parse.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.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"])

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

我从服务器收到 POST 测试的意外错误:

/Library/Frameworks/Python.framework/Versions/3.4/bin/python3.4 /Applications/PyCharm.app/Contents/helpers/pycharm/utrunner.py /Users/jweob/PyCharmProjects/coheat-network-control/testUiWebserver.py true
Testing started at 9:01 AM ...
[b'<!DOCTYPE html PUBLIC\n"-//W3C//DTD XHTML 1.0 Transitional//EN"\n"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n<html>\n<head>\n    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>\n    <title>500 Internal Server Error</title>\n    <style type="text/css">\n    #powered_by {\n        margin-top: 20px;\n        border-top: 2px solid black;\n        font-style: italic;\n    }\n\n    #traceback {\n        color: red;\n    }\n    </style>\n</head>\n    <body>\n        <h2>500 Internal Server Error</h2>\n        <p>The server encountered an unexpected condition which prevented it from fulfilling the request.</p>\n        <pre id="traceback">Traceback (most recent call last):\n  File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/cherrypy/_cprequest.py", line 663, in respond\n    self.body.process()\n  File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/cherrypy/_cpreqbody.py", line 996, in process\n    super(RequestBody, self).process()\n  File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/cherrypy/_cpreqbody.py", line 540, in process\n    proc(self)\n  File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/cherrypy/_cpreqbody.py", line 143, in process_urlencoded\n    qs = entity.fp.read()\n  File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/cherrypy/_cpreqbody.py", line 858, in read\n    return ntob(\'\').join(chunks)\nTypeError: sequence item 0: expected a bytes-like object, str found\n</pre>\n    <div id="powered_by">\n      <span>\n        Powered by <a href="http://www.cherrypy.org">CherryPy 3.8.0</a>\n      </span>\n    </div>\n    </body>\n</html>\n']

Failure
Traceback (most recent call last):
  File "/Users/jweob/PyCharmProjects/coheat-network-control/testUiWebserver.py", line 82, in test_echo
    response = self.webapp_request('/echo', method='POST', msg="hey there")
  File "/Users/jweob/PyCharmProjects/coheat-network-control/testUiWebserver.py", line 64, in webapp_request
    raise AssertionError("Unexpected error")
AssertionError: Unexpected error


Process finished with exit code 0

我认为该错误与我在 POST 正文中对消息进行编码的方式有关,但我无法确定我做错了什么。

我在这里找到了提示 here。我应该使用 io.BytesIO 而不是 io.StringIO,而不是

fd = StringIO(qs)

我用过

fd = BytesIO(qs.encode())

进行此更改后,该示例可以正常运行。