指南與踩坑:Python 單元測試

本文試圖總結編寫單元測試的流程,以及自己在寫單元測試時踩到的一些坑。如有遺漏,純屬必然,歡迎補充。

目錄概覽:

  • 編寫思想 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ù)寫測試是非常簡單的:我們只要

  1. 引入待測模塊
  2. 把參數(shù)傳給 待測模塊.待測函數(shù),取得返回值 actual_output 即為實際輸出
  3. 使用 assertEuqal 之類以 assert 開頭的函數(shù)來斷言:實際行為與預期行為一致。通常至少用 assertEqual 來斷言:實際輸出(actual_output)與預期輸出一致;在安排「實際輸出」與「預期輸出」在 assertEqual 中的參數(shù)順序時,我的做法是:第一個參數(shù)是「預期輸出」,第二個參數(shù)是「實際輸出」;理由是「預期輸出」的形狀和長度是固定的,「實際輸出」的形狀和長度通常會有各種變化(當測試出錯或函數(shù)沒有執(zhí)行預期行為時),把「實際輸出」安排在后面,在調試測試函數(shù)時,我們的視線關注點是固定的。
  4. 對于比較復雜的函數(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
  5. 其他的斷言方法可見 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())
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容