本文試圖總結編寫單元測試的流程,以及自己在寫單元測試時踩到的一些坑。如有遺漏,純屬必然,歡迎補充。
目錄概覽:
- 編寫思想 x 3
- 編寫方法
- 基本單元測試框架x3
- 對基于網(wǎng)站框架搭建的網(wǎng)絡應用進行單元測試
編寫思想
盡可能地按「單元」測試,重點是:保證待測單元內部的所有流程能按設想正確運行、返回預期結果。
假設我們有一個待測程序如下
# to_test.py
def func_01(param0101, param0102):
# 處理參數(shù) param0101 和 param0102 的代碼,如
ret = param0101 * param0102
# 假設結果保存在名為 ret 的變量中
return ret
def func_02(param0201, param0202):
# 處理參數(shù) param0201 和 param0202 的代碼,如
ret = param0201 / param0202
# 假設結果保存在名為 ret 的變量中
return ret
def call_other_funcs(param0101, param0102, param0201, param0202):
# 對傳入?yún)?shù) param0x0y 等進行一些處理后,存到了 param0x0y_changed 中。例如
param0x0y = param0x0y * 10
ret01 = func_01(param0101_changed, param0102_changed)
ret02 = func_02(param0201_changed, param0202_changed)
# 對得到的 ret01, ret02 進行一些處理,得到最終返回值 ret。例如
if ret01 and ret02:
ret = (ret01, ret02)
elif ret01 and (not ret02):
ret = (ret01, 0)
elif (not ret01) and ret02
ret = (0, ret02)
else:
ret = (0, 0)
return ret
要關注的點有2:
1. 每個測試僅保證 1 個單元的內部流程正確,即待測單元;這種正確性是不依賴于外部流程的正確性的
所以要假定該單元內調用的外部單元(如引入的模塊、函數(shù)等)能返回預期結果——這通過所謂的 Mock 來實現(xiàn),可理解為就是偽造出預期結果;至于那個外部單元能不能真的按給定輸入返回預期結果,那是那個外部單元對應的單元測試應該負責的事。
以上述示例程序 to_test.py 為例,本原則所關注的點體現(xiàn)在:
-
對 call_other_funcs 編寫單元測試時,我們僅關注上述除了 ret01 賦值和 ret02 賦值以外的代碼是否被正確執(zhí)行:
- 我們僅關注 param0101..param0202 在輸入后會不會執(zhí)行 +100 的操作乃至生成對應的 param0101_changed...param0202_changed
- 我們僅關注當 ret01 和 ret02 得到給定值后會不會執(zhí)行那套 if-elif-elif-else 以生成我們想要的 (ret01, ret02)、(ret01, 0)、(0, ret02) 三者之一
-
我們并不關注 ret01、ret02 是怎么由 param0101_changed...param0202_changed 通過 func_01、func_02 生成我們想要的值,那是對 func_01、func_02的測試應該負責的部分。
- 比如當我們想測第一個 if 時,我們就令 func_01 與 func_02 均返回 0;要測第 1 個 elif 時,我們就令 func_01 返回 1, 令 func_02 返回 0;遙測第 2 個 elif 時,我們就令 func_01 返回 0, 令 func_02 返回 1;要測 else 時,我們就令 func_01 與 func_02 均返回 0
- 假設我們不偽造 func_01 和 func_02 的返回結果,而是直接讓參數(shù) param0101_changed...param0202_changed 傳給 func_01 和 func_02、由它們來返回想要的結果如 (0, 0),我們實際上測試的不是上述的 call_other_funcs,而是在測下面的代碼——多測了這兩個函數(shù)的內部邏輯,換言之我們不僅僅在保證 1 個單元的正確性,而是在同時保證 3 個單元的正確性
def call_other_funcs_new(param0101, param0102, param0201, param0202):
# 對傳入?yún)?shù) param0x0y 等進行一些處理后,存到了 param0x0y_changed 中。例如
param0x0y = param0x0y * 10
ret01 = param0101_changed * param0102_changed # 我們還測了 func_01 的內部處理邏輯
ret02 = param0201_changed / param0202_changed # 我們還測了 func_02 的內部處理邏輯
# 對得到的 ret01, ret02 進行一些處理,得到最終返回值 ret。例如
if ret01 and ret02:
ret = (ret01, ret02)
elif ret01 and (not ret02):
ret = (ret01, 0)
elif (not ret01) and ret02
ret = (0, ret02)
else:
ret = (0, 0)
return ret
2. 測試要盡可能覆蓋到所有語句
仍然以上述函數(shù)為例,這里并不只是說測試需要覆蓋所有的 if-else 分支,而更著重于強調對上一條原則的配合,即:盡管我們要偽造一些函數(shù)的值,但我們也要保證對應的函數(shù)調用了指定的參數(shù)。
這是因為函數(shù)調用的參數(shù)可能依賴于調用函數(shù)前的代碼,因此確保函數(shù)調用了指定參數(shù),這種行為則確保了函數(shù)調用前那些(涉及到參數(shù)的)代碼能夠正確執(zhí)行。例如上述代碼中, 如果我們僅僅令 funcs_01 的返回值為某值,而沒有去檢查 funcs_01 到底調用的參數(shù)是不是我們預期的參數(shù),那么實際上我們就并沒有測試到像 param0101 = param0101 * 10 這樣的代碼。
上述 2 點是基本原則。在此之上,根據(jù)我踩的坑,還有 1 點想補充:
1. 測試要寫得「傻」一點
感謝首席測試小姐姐指出:測試不僅僅是為了保證功能正確,也是一份「代碼閱讀指南」——即當待測單元的行為不容易理解時,用戶可以通過閱讀這份代碼對應的單元測試來理解程序行為。
仍以上述對 to_test.py 的測試為例。(下面的 @mock.patch.object 與 mock_funcs_0x.return_value 配合,實現(xiàn)「偽造函數(shù)值」)
壞樣例:
from unittest import mock
import to_test
class ToTestTestCase(unittest.TestCase):
# ...其他測試函數(shù)...
@mock.patch.object(to_test, 'funcs01')
@mock.patch.object(to_test, 'funcs02')
def test_call_other_funcs(self, mock_funcs_01, mock_func_02):
funcs_ret_values = [
{"funcs_01": 1, "funcs_02": 1},
{"funcs_01": 1, "funcs_02": 0},
{"funcs_01": 0, "funcs_02": 1},
{"funcs_01": 0, "funcs_02": 0}
]
for funcs_ret in funcs_ret_values:
mock_funcs_01.return_value = funcs_ret["funcs_01"]
mock_funcs_02.return_value = funcs_ret["funcs_02"]
# 剩下的測試語句……
好樣例:
from unittest import mock
import to_test
class ToTestTestCase(unittest.TestCase):
# ...其他測試函數(shù)...
@mock.patch.object(to_test, 'funcs01')
@mock.patch.object(to_test, 'funcs02')
def test_call_other_funcs_if(self, mock_funcs_01, mock_func_02):
mock_funcs_01.return_value = 1
mock_funcs_02.return_value = 1
# 剩下的測試語句……
@mock.patch.object(to_test, 'funcs01')
@mock.patch.object(to_test, 'funcs02')
def test_call_other_funcs_elif_1(self, mock_funcs_01, mock_func_02):
mock_funcs_01.return_value = 1
mock_funcs_02.return_value = 0
# 剩下的測試語句……
@mock.patch.object(to_test, 'funcs01')
@mock.patch.object(to_test, 'funcs02')
def test_call_other_funcs_elif_2(self, mock_funcs_01, mock_func_02):
mock_funcs_01.return_value = 0
mock_funcs_02.return_value = 1
# 剩下的測試語句……
@mock.patch.object(to_test, 'funcs01')
@mock.patch.object(to_test, 'funcs02')
def test_call_other_funcs_else(self, mock_funcs_01, mock_func_02):
mock_funcs_01.return_value = 0
mock_funcs_02.return_value = 0
# 剩下的測試語句……
第一種寫法看起來更簡潔,更「模塊化」,對于單個函數(shù)的測試被封裝到了同一個函數(shù)中;但首先要面臨的問題就是:
每次你在閱讀測試函數(shù)是如何測試目標函數(shù)時,就要到上述的 list(如這里的 funcs_ret_values)中去查對應的函數(shù)到底被偽造成了什么值。
乍一看,這在需要偽造的函數(shù)值較少時看起來還不是大問題;但當需要偽造的函數(shù)數(shù)量多起來時,上面的 list of dict 就會變得冗長無比,非常不容易閱讀。
更嚴重的是第二個問題:
設想一種情景:你需要測試函數(shù)內部的 2 條不同邏輯 A 和 B,而這些不同邏輯會返回同樣的值 a。那么,由于測試是在for循環(huán)中進行的,當你發(fā)現(xiàn)希望返回 a 的時候沒有返回 a,你就不知道到底是在測邏輯 A 時出了錯,還是在測邏輯 B 時出了錯。于是你可能不得不非常仔細地去檢查樣例,手動再模擬一遍測試的過程,而且還要手動模擬 2 次:既要考慮模擬邏輯 A,也要考慮模擬邏輯 B。這加大了debug測試程序的難度。
第二種寫法雖然看上去更瑣碎,但由于測試粒度比較小,上述這兩個問題就都不復存在了。
編寫方法
基本單元測試框架
基本測試框架如下。先閱讀,再解釋:
假設我們有一個類似剛才的 to_test.py 的待測函數(shù) your_mod_name.py
# 引入單元測試模塊(unittest)和偽造模塊(mock)
import unittest
from unittest import mock
import your_mod_name
class YourModNameTestCase(unittest.TestCase):
def setUp(self):
"""
若每個單元測試前都要用到同一組數(shù)據(jù),則在這里編寫,如
self.var00 = val00
self.var01 = val01
不一定要有
"""
pass
def test_funcs_01(self):
"""
測試 funcs_01
所有要進行通過自動化測試框架的運行都以 test_ 開頭
"""
actual_output = your_mode_name.funcs_01(5, 2)
self.assertEqual(
10,
actual_output
)
def test_funcs_02(self):
"""
測試 funcs_02
"""
actual_output = your_mode_name.funcs_02(9, 3)
self.assertEqual(
3,
actual_output
)
@mock.patch.object(your_mod_name, 'funcs_02')
@mock.patch.object(your_mod_name, 'funcs_01')
def test_call_other_funcs_branch_01(self, mock_funcs_01, mock_funcs_02):
mock_funcs_01.return_value = (5 * 10) * (2 * 10)
mock_funcs_02.return_value = (9 * 10) / (3 * 10)
actual_output = your_mode_name.funcs_01(5, 2, 9, 3)
mock_funcs_01.assert_called_with(5 * 10, 2 * 10)
mock_funcs_02.assert_called_with(9 * 10, 3 * 10)
self.assertEqual(
((5 * 10) * (2 * 10), (9 * 10) / (3 * 10)),
actual_output
)
# 對 call_other_funcs 其他分支的測試
#
# ……
#
# 對其他函數(shù)的測試
以下解釋上述代碼
1. 最簡單的測試
對于像 funcs_01 和 funcs_02 這樣的函數(shù)寫測試是非常簡單的:我們只要
- 引入待測模塊
- 把參數(shù)傳給 待測模塊.待測函數(shù),取得返回值 actual_output 即為實際輸出
- 使用 assertEuqal 之類以 assert 開頭的函數(shù)來斷言:實際行為與預期行為一致。通常至少用 assertEqual 來斷言:實際輸出(actual_output)與預期輸出一致;在安排「實際輸出」與「預期輸出」在 assertEqual 中的參數(shù)順序時,我的做法是:第一個參數(shù)是「預期輸出」,第二個參數(shù)是「實際輸出」;理由是「預期輸出」的形狀和長度是固定的,「實際輸出」的形狀和長度通常會有各種變化(當測試出錯或函數(shù)沒有執(zhí)行預期行為時),把「實際輸出」安排在后面,在調試測試函數(shù)時,我們的視線關注點是固定的。
- 對于比較復雜的函數(shù)如 call_other_funcs 的測試,可能還需要使用 assert_called_with 等方法斷言函數(shù)調用的參數(shù),見 https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_called_with 。當需要斷言多次調用時(例如同一個函數(shù) funcs_01 被調用了 3 次,每次傳入了不同參數(shù)),可以考慮使用 assert_has_calls,見 https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_has_calls
- 其他的斷言方法可見 https://docs.python.org/3/library/unittest.html#assert-methods 對于 mock 對象的斷言方法可見 https://docs.python.org/3/library/unittest.mock.html#the-mock-class
2. 如何偽造一個對象
2.1 通用的 mock 框架
一般來說,我們把「偽造」稱為 mock,因為這就是 Python 中的偽造類的名字。
可以通過裝飾器 @mock.patch.object 的寫法「制造」 mock 對象,見 https://docs.python.org/3/library/unittest.mock.html#patch-object
以上述寫法為例:
@mock.patch.object(your_mod_name, 'funcs_02')
@mock.patch.object(your_mod_name, 'funcs_01')
def test_call_other_funcs_branch_01(self, mock_funcs_01, mock_funcs_02):
上面這三句的意思是:
把函數(shù) your_mod_name.funcs_01 偽造成 mock_funcs_01,把 your_mod_name.funcs_02 偽造成 mock_funcs_02。這里的 mock_funcs_01 和 mock_funcs_02 的變量名沒有特別的規(guī)定,這和一般的變量命名沒有兩樣,也可以命名為 mock_weird_name_007, mock_I_dont_know_why_101 等奇奇怪怪的名字。現(xiàn)在這樣命名只是為了方便理解。
在「制造」出 mock 對象后,我們要給 mock 對象賦值,因為偽造傳值才是我們的實際目標。以上述代碼為例,該目標通過這兩句來完成:
mock_funcs_01.return_value = (5 * 10) * (2 * 10)
mock_funcs_02.return_value = (9 * 10) / (3 * 10)
注意:上面這兩句,即所有偽造值的語句,都要在調用函數(shù)的語句前執(zhí)行,即下面這句之前執(zhí)行上述兩句:
actual_output = your_mode_name.funcs_01(5, 2, 9, 3)
而 return_value 也可以在裝飾器中就指定,例如:
@mock.patch.object(your_mod_name, 'funcs_02', return_value=(9 * 10) / (3 * 10))
@mock.patch.object(your_mod_name, 'funcs_01', return_value=(5 * 10) * (2 * 10))
def test_call_other_funcs_branch_01(self, mock_funcs_01, mock_funcs_02):
# 此后就不用再寫 mock_funcs_01.return_value = (5 * 10) * (2 * 10) 這樣的句子了
除了通過裝飾器來偽造,還可以通過上下文管理器(context manager)的方法來偽造 。同樣是類似上述代碼,可以寫為:
@mock.patch.object(your_mod_name, 'funcs_02')
@mock.patch.object(your_mod_name, 'funcs_01')
def test_call_other_funcs_branch_01(self, mock_funcs_01, mock_funcs_02):
with mock.patch.object(your_mod_name, 'funcs_01', \
return_value=(5 * 10) * (2 * 10)) as mock_funcs_01,
mock.patch.object(your_mod_name, 'funcs_02', \
return_value=(9 * 10) / (3 * 10)) as mock_funcs_02:
actual_output = your_mode_name.funcs_01(5, 2, 9, 3)
mock_funcs_01.assert_called_with(5 * 10, 2 * 10)
mock_funcs_02.assert_called_with(9 * 10, 3 * 10)
self.assertEqual(
((5 * 10) * (2 * 10), (9 * 10) / (3 * 10)),
actual_output
)
2.2 特殊語句順序
所有的偽造值語句,必須要在調用 actual_output 的賦值語句(即實際調用目標函數(shù))之前執(zhí)行。
所有的斷言語句,包括 assertEqual 或 assert_called_with,必須要在調用 actual_output 的賦值語句(即實際調用目標函數(shù))之后執(zhí)行。這是因為需要斷言的值、對象都要在函數(shù)執(zhí)行后才會產(chǎn)生(需要斷言的參數(shù)調用也是一種值,也要在函數(shù)執(zhí)行后,才會在內存中留下「痕跡」,在執(zhí)行之前,程序無法知道待測函數(shù)內部調用的其他函數(shù)到底調用了什么參數(shù))。
要注意:調用 assert_called_with 的一定是某個 mock 對象而非 self,這是與 assertEqual 最大的區(qū)別
2.3 使用場景
更具體地說,分為 2 種情況:(1)在諸多測試用例中,每個對象的返回值之間各自獨立;(2)這些返回值之間符合某種函數(shù)關系
根據(jù)這 2 種情況,對應的有 4 種偽造對象的寫法。其中 2.3.1 對應第 (1) 種情況,2.3.2~2.3.4 對應第 (2) 種情況:
2.3.1 一般外部模塊、網(wǎng)絡連接、數(shù)據(jù)庫連接
常見于偽造一般外部模塊(自己或團隊其他成員寫的模塊、開源庫模塊等)、數(shù)據(jù)庫連接、網(wǎng)絡連接的返回結果。都是類似上面對 funcs_01 的 mock 方法。給出 2 個在數(shù)據(jù)庫連接和網(wǎng)絡連接方面的 mock 示例代碼:
假設使用數(shù)據(jù)庫連接的原始代碼為:
# your_mod_name.py
import db_conn
# some other function code ...
def funcs_with_db_connection(params):
# some code ...
answers = db_conn.query(sql) # sql 是指定的 SQL 語句字符串
# some code to process answers
# 假定 ret 是返回變量
return ret
則對應的 mock 代碼為:
# 數(shù)據(jù)庫連接
import your_mod_name
@mock.patch.object(your_mod_name.db_conn, 'query', return_value=['000001', '000002'])
def test_funcs_with_db_connection(self, mock_db_conn):
# params 是參數(shù)
# 在有了上面
actual_output = your_mod_name.funcs_with_db_connection(params)
網(wǎng)絡連接則以 requests.get 為例:
# your_mod_name.py
import requests
# some other function code ...
def funcs_with_requests(params):
# some code ...
answers = requests.get('your_url_to_site') # 從 your_url_to_site 獲取信息
# some code to process answers
# 假定 ret 是返回變量
return ret
這時候返回的對象可能有多個屬性如 status_coe 和 text,而且都要用上。那么此時對應的 mock 代碼可寫成:
@mock.patch.object(your_mod_name.requests, 'get')
def test_funcs_with_requests(self, mock_requests_get)
mock_response = mock.Mock()
mock_response.status_code = status
mock_response.text = {'key01': 'val01', 'key02': 'val02'}
mock_requests.return_value = mock_response
2.3.2 偽造成一個指定函數(shù):轉發(fā)輸入
相當于把原始函數(shù)的輸入值「轉發(fā)」到指定函數(shù)上。例如原始代碼為:
# your_mod_name.py
def format_answers(raw_string):
# 處理 raw_string 非常復雜的處理邏輯
# 假設處理完后保存到 good_string 中
return good_string
def funcs_with_format_answers(params):
# 某些代碼生成了原始答案字符串 raw_string
answers = format_answers(raw_string)
# some code ... 返回 ret
return ret
比如在測試函數(shù)中,我多次調用了該函數(shù),但我不想對每次 mock 都指定一個值,那么可以這么做:
import your_mod_name
def mock_format_answers(input_string):
return input_string
@mock.patch.object(your_mod_name, 'format_answers', mock_format_answers):
def test_funcs_with_format_answers(self):
# some code for testing
注意到當我們指定了轉發(fā)目標后,實際上指定了「制造」的 mock 對象為我們設計好的函數(shù),這樣就不需要在函數(shù)頭中再寫 mock_format_answers 了(如果寫,反而會報錯)。
2.3.3 從一個偽造類生成一個偽造對象
設想一個情況:你在原始函數(shù)中調用了某個類 ClassA 生成了實例 instance_A,并在原始函數(shù)中使用了該類的多個方法。那么一次次 mock 這個函數(shù)的一個個方法,可能看著或寫著繁瑣。在這種情況下,我們就可以通過將對應的類轉發(fā)到我們設計好的偽造類上,并在偽造類下定義需要 mock 的方法,從而 mock 一個類就相當于 mock 了和該類相關的所有方法。
一個簡單的例子是:某個函數(shù)內部調用了 time 這個類的 time() 和 sleep() 方法,例如:
# your_mod_name.py
import time
def funcs_with_time(params):
t_start = time.time()
# some code ...
t_cost = time.time() - t_start
ret = []
while t_cost <= 10:
# do something
time.sleep(0.5)
t_cost = time.time() - t_start
if t_cost > 5:
ret.append('good')
return ret
在我們編寫測試的時候,如果不將 time 這個類 mock 掉,那么程序的行為就不可預測:程序運行時是一個隨機行為,我們如果不能「控制時間」,就不能保證測試函數(shù)在測試時能走到目標函數(shù)中的指定分支。
我們只要在測試函數(shù)中這么寫即可:
import your_mod_name
class MockTime(object):
"""
用于 time 的 Mock 類
"""
def __init__(self):
self.time_count = -0.5 # 配合 time() 方法使 ts_start = 0
def time(self):
"""
每次對象被調用時會運行這里的代碼。
"""
if self.time_count == -0.5:
self.time_count = 0.0
return self.time_count
def sleep(self, gap):
"""
0.5 的增量保證能在 cost 超過 10 之前觸發(fā):銷毀 Token,返回 URL
"""
self.time_count += 0.5
return
mock_time_helper = MockTime()
class YourModNameTestCase(unittest.TestCase):
# some code for testing other functions ...
@mock.patch.object(your_mod_name, 'time', mock_time_helper)
def test_funcs_with_time(self):
# 這樣就可以將不可控的「程序運行時」變?yōu)榭煽氐摹赣嫈?shù)器」
# some code for testing ...
2.3.4 如何偽造內置函數(shù)(built-in functions)的返回結果,如open(path_to_file).readlines()
這其實是 2.3.3 這個情況的一個特例,但也是一個容易讓人抓狂的點。比如有時候我們需要測試的函數(shù)內有一個 open 函數(shù),在打開文件后還調用了 readlines() 方法。那么我們如何 mock 掉 open?或者 open 返回的對象類名是啥,我能不能去 mock 那個類對應的 readlines() 方法?其實這個問題的關鍵在于:內置函數(shù)的類是什么?
答案是:builtins https://docs.python.org/3/library/builtins.html
那么之后就能夠像 2.3.3 一樣去處理了。
例如原始代碼是
# your_mod_name.py
def funcs_with_open(params):
# some code ...
tokens = [line.strip('\n') for line in open('path_to_file').readlines()]
return tokens
對應的測試函數(shù)中可以這么寫
import unittest
import builtins
import your_mod_name
class MockOpen(object):
"""
內置函數(shù) open 的 mock 類
"""
def __init__(self, data):
assert isinstance(data, list), '請輸入一個列表: {}'.format((data))
self.data = data
def __call__(self, blabla):
return self
def readlines(self):
return self.data
class YourModNameTestCase(unittest.TestCase):
# some code to test other functions
def test_funcs_with_open(self):
with mock.patch('builtins.open', MockOpen(['test00\n', 'test01\n']))
# some code to get params
actual_output = your_mod_name.funcs_with_open(params)
self.assertEqual(
['test00', 'test01'],
actual_output
)
對基于網(wǎng)站框架搭建的網(wǎng)絡應用進行單元測試
和編寫網(wǎng)絡應用一樣:編寫網(wǎng)絡應用(即涉及到網(wǎng)絡通信的程序)的單元測試,與編寫一般程序的單元測試基本一致,最大的差別就在于:網(wǎng)絡應用和網(wǎng)絡應用的單元測試一般需要額外關注:
- 路由:要通過哪個URI進行數(shù)據(jù)操作,例如要從哪里去GET數(shù)據(jù)、把數(shù)據(jù)POST到哪里(一般必須處理)
- 狀態(tài)碼:返回狀態(tài)的設置和捕捉(不一定要捕捉或設置)
這里就不做太多展開,在 2 個框架下各給出 1 個例子并做簡要解釋,更多情況請參閱對應的文檔,或等我日后填坑(然后可能就不知不覺棄坑了?)
[Flask]
更多有關 Flask 的測試方法,見
https://pythonhosted.org/Flask-Testing/
http://flask.pocoo.org/docs/0.12/testing/
原程序
@app.route('/userapi/get_phone_number', methods=['POST'])
def get_phone_number():
# some code to get phone number
對應測試程序中如何調用該程序:
# request_data 是已經(jīng)處理過的要 POST 的 JSON
rv = self.client.post('/userapi/get_phone_number', data=request_data)
注意到這里的 '/userapi/get_phone_number' 就是調用路由
[Tornado]
更多有關 Tornado 的測試方法,見 http://www.tornadoweb.org/en/stable/testing.html
原項目中由這個程序指定了程序路由:
# server.py
class Application(tornado.web.Application):
def __init__(self):
handlers = [
# some other handlers ...
(r"/qaapi/qa", RESTfulAPIHandler) # 已有 RESTfulAPIHandler.py 是對應應用
]
tornado.web.Application.__init__(self, handlers)
# some other codes ...
要測試時:
uri = '/qaapi/qa?userid={}&token={}'.format(userid, token)
data = get_data()
response = self.fetch(uri, method="POST", body=data)
self.assertEqual(400, response.code)
self.assertEqual('expected_output', response.buffer.getvalue())