使用Python的flask和Nose对Twilio应用进行单元测试

it2024-10-02  45

让我们削减一些代码

首先,我们将在安装了Twilio和Flask模块的Python环境中打开一个文本编辑器,并开发出一个简单的应用程序,该应用程序将使用动词和名词创建一个Twilio会议室。

这是我们将其命名为app的文件的简要介绍 。py:

from flask import Flask                                                         from twilio import twiml                                                       app = Flask(__name__)                                                           @app.route('/conference', methods=['POST'])                                     def voice():    response = twiml.Response()      with response.dial() as dial:                                                  dial.conf("Rob's Blog Party")                                        return str(response) if __name__ == "__main__":    app.debug = True    app.run(port=5000)

现在让我们测试一下

我认为这段代码可能是正确的,但是让我们通过编写快速的单元测试来确保。为此,我们将打开另一个名为test_app的文件 。py。在该文件中,我们将导入我们的应用程序,并在Python标准库中使用unittest定义一个单元测试 。然后,我们将使用Flask测试客户端向应用发出测试请求,并查看应用是否抛出错误。

from flask import Flask from twilio import twiml # 定义我们的应用程序 app = Flask(__name__) # NoseDefine要用作会议室的端点 @app.route('/conference', methods=['POST']) def voice():    response = twiml.Response()    with response.dial() as dial: # 现在我们使用正确的属性。        dial.conference("Rob's Blog Party")    return str(response) # 在端口5000上以调试模式运行应用程序 if __name__ == "__main__":    app.debug = True    app.run(port=5000)

后,我们使用Nose运行单元测试通过发出以下命令,Nose将遍历我们的单元测试文件,找到所有 TestCase对象并执行每个以test_为前缀的方法 :

nosetests - v test_app 。py

哦,饼干-好像我们有个错误。

test_conference (test_intro.TestConference) ... FAIL ====================================================================== FAIL: test_conference (test_intro.TestConference) ---------------------------------------------------------------------- Traceback (most recent call last):  File "/home/rspectre/workspace/test_post/test_intro.py", line 16, in test_conference    self.assertEquals(response.status, "200 OK") AssertionError: '500 INTERNAL SERVER ERROR' != '200 OK' -------------------- >> begin captured logging << -------------------- app: ERROR: Exception on /conference [POST] Traceback (most recent call last): File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1504,    in wsgi_app    response = self.full_dispatch_request() File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1264,    in full_dispatch_request     rv = self.handle_user_exception(e)   File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1262,    in full_dispatch_request     rv = self.dispatch_request()   File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1248,    in dispatch_request return self.view_functions[rule.endpoint](**req.view_args) File "/home/rspectre/workspace/test_post/app.py", line 13,    in voice   dial.conf("Rob's Blog Party") AttributeError: 'Dial' object has no attribute 'conf' --------------------- >> end captured logging << --------------------- ---------------------------------------------------------------------- Ran 1 test in 0.009s FAILED (failures=1)

天啊 用于会议的TwiML名词的名称不是“ Conf”,而是“ Conference”。让我们重新访问我们的 应用程序。py文件并更正错误。

from flask import Flask from twilio import twiml # Define our app app = Flask(__name__) # 定义要用作会议室的终结点 @app.route('/conference', methods=['POST']) def voice():    response = twiml.Response()    with response.dial() as dial: # 现在我们使用正确的属性。        dial.conference("Rob's Blog Party")    return str(response) # 在端口5000上以调试模式运行应用程序 if __name__ == "__main__":    app.debug = True    app.run(port=5000)

现在更正了会议线,我们可以使用与上面相同的命令重新运行测试:

rspectre@drgonzo:~/workspace/test_post$ nosetests -v test_app.py test_conference (test_app.TestConference) ... ok test_conference_valid (test_app.TestConference) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.011s OK

太棒了 而且,我们不必拿起电话来找出错误。

现在,让我们确保此代码可以实现我们想要的功能

确保代码不会引发错误是很好的第一步,但是我们还想确保Twilio应用程序能够按预期方式执行。首先,我们需要检查应用程序是否返回了Twilio可以解释的响应,请确保它正在创建有效的Dial动词,最后确保Dial指向正确的会议室。

为了提供帮助,我们将使用ElementTree,它是Python标准库中的XML解析器。这样,我们可以像Twilio一样解释TwiML响应。让我们看看如何将其添加到 test_app 。py:

import unittest from app import app # 导入XML解析器 from xml.etree import ElementTree class TestConference(unittest.TestCase):    def test_conference(self): # 保留以前的测试。        self.test_app = app.test_client()        response = self.test_app.post('/conference', data={'From': '+15556667777'})        self.assertEquals(response.status, "200 OK")    def test_conference_valid(self): # 创建一个新的测试来验证我们的TwiML是否在做它应该做的事情。        self.test_app = app.test_client()        response = self.test_app.post('/conference', data={'From': '+15556667777'}) # 将结果解析为ElementTree对象        root = ElementTree.fromstring(response.data) # 断言根元素是响应标记        self.assertEquals(root.tag, 'Response',                "Did not find  tag as root element " \                "TwiML response.") # 断言响应有一个拨号动词        dial_query = root.findall('Dial')        self.assertEquals(len(dial_query), 1,                "Did not find one Dial verb, instead found: %i " %                len(dial_query)) # 断言拨号动词只有一个名词        dial_children = list(dial_query[0])        self.assertEquals(len(dial_children), 1,                "Dial does not go to one noun, instead found: %s" %                len(dial_children)) # 断言拨入会议名词        self.assertEquals(dial_children[0].tag, 'Conference',                "Dial is not to a Conference, instead found: %s" %                dial_children[0].tag) # Assert Conference is Rob's Blog Party        self.assertEquals(dial_children[0].text, "Rob's Blog Party",                "Conference is not Rob's Blog Party, instead found: %s" %                dial_children[0].text)

现在使用Nose运行两个测试:

rspectre@drgonzo:~/workspace/test_post$ nosetests -v test_app.py test_conference (test_app.TestConference) ... ok test_conference_valid (test_app.TestConference) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.011s OK

现在,我们有信心该应用程序除了返回适当的响应外,还会执行我们想要的操作。

我们的测试以供重用

非常高兴知道我们的新Twilio端点无需手动测试即可工作,但是Twilio应用程序很少使用单个webhook端点。随着应用程序复杂性的增加,我们可以看到这两个测试将重复很多代码。让我们看看是否可以将测试重构为通用测试用例,以用于将来构建的任何Twilio Webhook端点。

为此,我们将创建一个通用的 TwiMLTest类,并利用内置的 setUp ()方法在每个测试中自动实例化Flask测试客户端。

import unittest from app import app from xml.etree import ElementTree class TwiMLTest(unittest.TestCase): def setUp(self): # 创建每个测试用例都可以使用的测试应用程序。        self.test_app = app.test_client()

伟大的开始–现在让我们创建一个辅助方法,该方法接受响应并进行TwiML工作的基本验证。

import unittest from app import app from xml.etree import ElementTree class TwiMLTest(unittest.TestCase): def setUp(self): # 创建每个测试用例都可以使用的测试应用程序。        self.test_app = app.test_client() def assertTwiML(self, response): # 检查错误。        self.assertEquals(response.status, "200 OK") # 将结果解析为ElementTree对象        root = ElementTree.fromstring(response.data) # 断言根元素是响应标记        self.assertEquals(root.tag, 'Response',                "Did not find  tag as root element " \                "TwiML response.")

最后,让我们创建两个其他的辅助方法,而不是为每次测试创建一个新的POST请求,这些方法将为调用和消息创建Twilio请求,我们可以使用自定义参数轻松地对其进行扩展。让我们向test_app添加一个新类 。py。

import unittest from app import app from xml.etree import ElementTree class TwiMLTest(unittest.TestCase): def setUp(self):        self.test_app = app.test_client() def assertTwiML(self, response):        self.assertEquals(response.status, "200 OK")        root = ElementTree.fromstring(response.data)        self.assertEquals(root.tag, 'Response',                "Did not find  tag as root element " \                "TwiML response.") def call(self, url='/voice', to='+15550001111',            from_='+15558675309', digits=None, extra_params=None): """Simulates Twilio Voice request to Flask test client        Keyword Args:            url: The webhook endpoint you wish to test. (default '/voice')            to: The phone number being called. (default '+15500001111')             from_: The CallerID of the caller. (default '+1558675309')            digits: DTMF input you wish to test (default None)            extra_params: Dictionary of additional Twilio parameters you                wish to simulate, like QueuePosition or Digits. (default: {})        Returns:            Flask test client response object.        """ # 为Twilio接收的消息设置一些常用参数。        params = {            'CallSid': 'CAtesting',            'AccountSid': 'ACxxxxxxxxxxxxx',            'To': to,            'From': from_,            'CallStatus': 'ringing',            'Direction': 'inbound',            'FromCity': 'BROOKLYN',            'FromState': 'NY',            'FromCountry': 'US',            'FromZip': '55555'} # 添加模拟DTMF输入。        if digits:            params['Digits'] = digits # 添加默认情况下未定义的额外参数。        if extra_params:            params = dict(params.items() + extra_params.items()) # 返回应用程序的响应。        return self.test_app.post(url, data=params) def message(self, body, url='/message', to="+15550001111",            from_='+15558675309', extra_params={}): """Simulates Twilio Message request to Flask test client        Args:            body: The contents of the message received by Twilio.        Keyword Args:            url: The webhook endpoint you wish to test. (default '/sms')             to: The phone number being called. (default '+15500001111')            from_: The CallerID of the caller. (default '+15558675309')            extra_params: Dictionary of additional Twilio parameters you                wish to simulate, like MediaUrls. (default: {})        Returns:            Flask test client response object.        """ # 为Twilio接收的消息设置一些常用参数。        params = {            'MessageSid': 'SMtesting',            'AccountSid': 'ACxxxxxxx',            'To': to,            'From': from_,            'Body': body,            'NumMedia': 0,            'FromCity': 'BROOKLYN',            'FromState': 'NY',            'FromCountry': 'US',            'FromZip': '55555'} # 添加默认情况下未定义的额外参数。        if extra_params:            params = dict(params.items() + extra_params.items()) # 返回应用程序的响应。        return self.test_app.post(url, data=params)

太好了–现在,我们可以使用新的帮助器方法重构会议的原始测试,从而使测试更短:

import unittest from app import app from xml.etree import ElementTree class TwiMLTest(unittest.TestCase): def setUp(self):        self.test_app = app.test_client() def assertTwiML(self, response):        self.assertEquals(response.status, "200 OK")        root = ElementTree.fromstring(response.data)        self.assertEquals(root.tag, 'Response',                "Did not find  tag as root element " \                "TwiML response.") def call(self, url='/voice', to='+15550001111',            from_='+15558675309', digits=None, extra_params=None): """Simulates Twilio Voice request to Flask test client        Keyword Args:            url: The webhook endpoint you wish to test. (default '/voice')            to: The phone number being called. (default '+15550001111')            from_: The CallerID of the caller. (default '+15558675309')            digits: DTMF input you wish to test (default None)            extra_params: Dictionary of additional Twilio parameters you                wish to simulate, like QueuePosition or Digits. (default: {})        Returns:            Flask test client response object.        """ # Set some common parameters for messages received by Twilio.        params = {            'CallSid': 'CAtesting',            'AccountSid': 'ACxxxxxxxxxxxxx',            'To': to,            'From': from_,            'CallStatus': 'ringing',            'Direction': 'inbound',            'FromCity': 'BROOKLYN',            'FromState': 'NY',            'FromCountry': 'US',            'FromZip': '55555'} # Add simulated DTMF input.        if digits:            params['Digits'] = digits # Add extra params not defined by default.        if extra_params:            params = dict(params.items() + extra_params.items()) # Return the app's response.        return self.test_app.post(url, data=params) def message(self, body, url='/message', to="+15550001111",            from_='+15558675309', extra_params={}): """Simulates Twilio Message request to Flask test client         Args:            body: The contents of the message received by Twilio.        Keyword Args:            url: The webhook endpoint you wish to test. (default '/sms')            to: The phone number being called. (default '+15550001111')            from_: The CallerID of the caller. (default '+15558675309')            extra_params: Dictionary of additional Twilio parameters you                wish to simulate, like MediaUrls. (default: {})        Returns:            Flask test client response object.        """ # 为Twilio接收的消息设置一些常用参数。        params = {            'MessageSid': 'SMtesting',            'AccountSid': 'ACxxxxxxx',            'To': to,            'From': from_,            'Body': body,            'NumMedia': 0,            'FromCity': 'BROOKLYN',            'FromState': 'NY',            'FromCountry': 'US',            'FromZip': '55555'} # 添加默认情况下未定义的额外参数。        if extra_params:            params = dict(params.items() + extra_params.items()) # 返回应用程序的响应。        return self.test_app.post(url, data=params) class TestConference(TwiMLTest): def test_conference(self):        response = self.call(url='/conference')        self.assertTwiML(response) def test_conference_valid(self): # 创建一个新的测试来验证我们的TwiML是否在做它应该做的事情。        response = self.call(url='/conference') # 将结果解析为ElementTree对象        root = ElementTree.fromstring(response.data) # 断言响应有一个拨号动词        dial_query = root.findall('Dial')        self.assertEquals(len(dial_query), 1,                "Did not find one Dial verb, instead found: %i " %                len(dial_query)) # 断言拨号动词只有一个名词        dial_children = list(dial_query[0])        self.assertEquals(len(dial_children), 1,                "Dial does not go to one noun, instead found: %s" %                len(dial_children)) # 断言拨入会议名词        self.assertEquals(dial_children[0].tag, 'Conference',                "Dial is not to a Conference, instead found: %s" %                dial_children[0].tag) # Assert Conference is Rob's Blog Party        self.assertEquals(dial_children[0].text, "Rob's Blog Party",                "Conference is not Rob's Blog Party, instead found: %s" %                dial_children[0].text)

完美–让我们使用Nose进行测试,看看我们是否成功。

rspectre@drgonzo:~/workspace/test_post$ nosetests -v test_app.py test_conference (test_app.TestConference) ... ok test_conference_valid (test_app.TestConference) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.014s OK

世界一切都很好。

进行测试

使用我们针对Twilio应用程序的通用测试用例,现在编写测试既快速又简单。我们编写了一个快速的会议应用程序,使用Nose对它进行了测试,然后将这些测试重构为可以与所有应用程序一起使用的通用案例。通过使用此测试用例,可以快速轻松地测试我们基于Flask构建的Twilio应用程序,从而减少了用手机手动测试所花费的时间,并减少了您听到可怕的“应用程序错误”声音的次数。

更多精彩推荐,请关注我们

扫码关注更多精彩

你点的每个赞,我都认真当成了喜欢

最新回复(0)