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

2020-06-17 16:16:13 浏览数 (1)

让我们削减一些代码

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

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

代码语言:javascript复制
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测试客户端向应用发出测试请求,并查看应用是否抛出错误。

代码语言:javascript复制
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

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

代码语言:javascript复制
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文件并更正错误。

代码语言:javascript复制
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)

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

代码语言:javascript复制
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:

代码语言:javascript复制
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运行两个测试:

代码语言:javascript复制
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测试客户端。

代码语言:javascript复制
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工作的基本验证。

代码语言:javascript复制
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。

代码语言:javascript复制
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)

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

代码语言:javascript复制
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进行测试,看看我们是否成功。

代码语言:javascript复制
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 人点赞