28. Flask 使用unittest進行單元測試

為什么要測試?

Web程序開發(fā)過程一般包括以下幾個階段:需求分析龙优,設計階段羊异,實現(xiàn)階段,測試階段彤断。其中測試階段通過人工或自動來運行測試某個系統(tǒng)的功能野舶。目的是檢驗其是否滿足需求,并得出特定的結果宰衙,以達到弄清楚預期結果和實際結果之間的差別的最終目的平道。

測試的分類:

測試從軟件開發(fā)過程可以分為:單元測試、集成測試供炼、系統(tǒng)測試等一屋。在眾多的測試中,與程序開發(fā)人員最密切的就是單元測試袋哼,因為單元測試是由開發(fā)人員進行的冀墨,而其他測試都由專業(yè)的測試人員來完成。所以作為開發(fā)人員主要需要學習單元測試涛贯。

什么是單元測試诽嘉?

程序開發(fā)過程中,寫代碼是為了實現(xiàn)需求弟翘。當我們的代碼通過了編譯虫腋,只是說明它的語法正確,功能能否實現(xiàn)則不能保證稀余。 因此悦冀,當我們的某些功能代碼完成后,為了檢驗其是否滿足程序的需求睛琳『畜。可以通過編寫測試代碼,模擬程序運行的過程师骗,檢驗功能代碼是否符合預期茁影。

單元測試就是開發(fā)者編寫一小段代碼,檢驗目標代碼的功能是否符合預期丧凤。通常情況下募闲,單元測試主要面向一些功能單一的模塊進行。

舉個例子:一部手機有許多零部件組成愿待,在正式組裝一部手機前浩螺,手機內部的各個零部件靴患,CPU、內存要出、電池鸳君、攝像頭等,都要進行測試患蹂,這就是單元測試或颊。

在Web開發(fā)過程中,單元測試實際上就是一些“斷言”(assert)代碼传于。

斷言就是判斷一個函數(shù)或對象的一個方法所產(chǎn)生的結果是否符合你期望的那個結果囱挑。 python中assert斷言是聲明布爾值為真的判定,如果表達式為假會發(fā)生異常沼溜。單元測試中平挑,一般使用assert來斷言結果。

斷言方法的使用:

# 定義一個list
In [6]: a = [1,3,5,7,9]

In [7]: b = 3

# 斷言判斷 b 是否存在 a 中系草,如果正確通熄,則不會報錯
In [8]: assert b in a

# 斷言如果報錯,可以自定義打印錯誤信息找都,這里定義錯誤為 False
In [9]: assert b not in a, 'False'
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-9-a3961e666e68> in <module>
----> 1 assert b not in a, 'False'

AssertionError: False

# 斷言報錯唇辨,但是如果沒有自定義錯誤信息,則只會顯示錯誤
In [10]: assert b not in a
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-10-57ad4b59ee88> in <module>
----> 1 assert b not in a

AssertionError:

In [11]: 

斷言語句類似于:

if not expression:
    raise AssertionError

常用的斷言方法:

assertEqual     如果兩個值相等能耻,則pass
assertNotEqual  如果兩個值不相等助泽,則pass
assertTrue      判斷bool值為True,則pass
assertFalse     判斷bool值為False嚎京,則pass
assertIsNone    不存在,則pass
assertIsNotNone 存在隐解,則pass

如何測試鞍帝?

寫一個斐波那契數(shù)列 Fibonacci 來進行測試,驗證以下的數(shù)字是否符合斐波那契數(shù)列煞茫。

可以測試的數(shù)字:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233帕涌,377,610续徽,987蚓曼,1597,2584钦扭,4181纫版,6765,
# 斐波那契數(shù)列 Fibonacci
def fibo(x):
    if x == 0:
        resp = 0
    elif x == 1:
        resp = 1
    else:
        return fibo(x-1) + fibo(x-2)
    return resp
assert fibo(5) == 5

測試執(zhí)行判斷斷言如下:

單元測試的基本寫法:

首先客情,定義一個類其弊,繼承自unittest.TestCase

import unittest
class TestClass(unitest.TestCase):
    pass

其次癞己,在測試類中,定義兩個測試方法

import unittest
class TestClass(unittest.TestCase):

    #該方法會首先執(zhí)行梭伐,方法名為固定寫法
    def setUp(self):
        pass

    #該方法會在測試代碼執(zhí)行完后執(zhí)行痹雅,方法名為固定寫法
    def tearDown(self):
        pass

最后,在測試類中糊识,編寫測試代碼

import unittest
class TestClass(unittest.TestCase):

    #該方法會首先執(zhí)行绩社,相當于做測試前的準備工作
    def setUp(self):
        pass

    #該方法會在測試代碼執(zhí)行完后執(zhí)行,相當于做測試后的掃尾工作
    def tearDown(self):
        pass
    #測試代碼
    def test_app_exists(self):
        pass

看清楚了上面關于unittest測試框架的基本寫法之后赂苗,下面來寫一個登錄的視圖函數(shù)愉耙,然后再寫一個視圖函數(shù)的單元測試。

登錄視圖函數(shù)的單元測試

1.編寫一個模擬登錄的視圖函數(shù) login.py

from flask import Flask, request, jsonify


app = Flask(__name__)


@app.route("/login", methods=["POST"])
def login():
    """登錄"""
    name = request.form.get("name")
    password = request.form.get("password")

    # ""  0  [] () {} None 在邏輯判斷時都是假
    if not all([name, password]):
        # 表示name或password中有一個為空或者都為空
        return jsonify(code=65535, message="參數(shù)不完整")

    if name == "admin" and password =="123456":
        return jsonify(code=0, message="OK")
    else:
        return jsonify(code=65535, message="用戶名或密碼錯誤")


if __name__ == '__main__':
    app.run(debug=True)

2. 使用postman測試login登錄

首先輸入正確的用戶名和密碼測試哑梳,如下:

然后去除用戶名或者密碼劲阎,缺少參數(shù)進行請求,如下:

故意輸錯密碼進行請求鸠真,如下:

通過postman測試接口這三種情況是可以的悯仙,但是如果每次都要手動去進行這樣的單元測試,就會感覺很麻煩了吠卷。

那么下面可以將這三種情況寫成單元測試的代碼锡垄,來避免重復測試。

3.編寫單元測試代碼 test_login.py

import unittest
from login import app
import json

class TestLogin(unittest.TestCase):
    """定義測試案例"""
    def setUp(self):
        """在執(zhí)行具體的測試方法前祭隔,先被調用"""

        self.app = app
        # 激活測試標志
        app.config['TESTING'] = True

        # 可以使用python的http標準客戶端進行測試
        # urllib  urllib2  requests

        # 在這里,使用flask提供的測試客戶端進行測試
        self.client = app.test_client()

    def test_empty_name_password(self):
        """測試模擬場景货岭,用戶名或密碼不完整"""
        # 使用客戶端向后端發(fā)送post請求, data指明發(fā)送的數(shù)據(jù),會返回一個響應對象
        response = self.client.post("/login", data={})

        # respoonse.data是響應體數(shù)據(jù)
        resp_json = response.data

        # 按照json解析
        resp_dict = json.loads(resp_json)

        # 使用斷言進行驗證:是否存在code字符串在字典中
        self.assertIn("code", resp_dict)

        # 獲取code的返回碼的值疾渴,驗證是否為錯誤碼 65535
        code = resp_dict.get("code")
        self.assertEqual(code, 65535)

        # 測試只傳name
        response = self.client.post("/login", data={"name": "admin"})

        # respoonse.data是響應體數(shù)據(jù)
        resp_json = response.data

        # 按照json解析
        resp_dict = json.loads(resp_json)

        # 使用斷言進行驗證
        self.assertIn("code", resp_dict)

        # 驗證錯誤碼 65535
        code = resp_dict.get("code")
        self.assertEqual(code, 65535)

        # 驗證返回信息
        msg = resp_dict.get('message')
        self.assertEqual(msg, "參數(shù)不完整")

    def test_wrong_name_password(self):
        """測試用戶名或密碼錯誤"""
        # 使用客戶端向后端發(fā)送post請求, data指明發(fā)送的數(shù)據(jù)千贯,會返回一個響應對象
        response = self.client.post("/login", data={"name": "admin", "password": "123456789"})

        # respoonse.data是響應體數(shù)據(jù)
        resp_json = response.data

        # 按照json解析
        resp_dict = json.loads(resp_json)

        # 使用斷言進行驗證
        self.assertIn("code", resp_dict)

        # 驗證錯誤碼
        code = resp_dict.get("code")
        self.assertEqual(code, 65535)

        # 驗證返回信息
        msg = resp_dict.get('message')
        self.assertEqual(msg, "用戶名或密碼錯誤")


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

執(zhí)行測試如下:

從上面可以看出,大部分的Flask框架的單元測試就是這樣的處理流程搞坝。下面再提供一個數(shù)據(jù)庫單元測試的示例搔谴。

數(shù)據(jù)庫單元測試:

數(shù)據(jù)單元測試的基本步驟方法如下:
1.替換使用一個創(chuàng)建的testdb測試庫,避免影響項目的實際數(shù)據(jù)庫
2.導入代碼中構建數(shù)據(jù)庫的模型類桩撮、app敦第、db等對象,創(chuàng)建數(shù)據(jù)庫以及創(chuàng)建數(shù)據(jù)
3.斷言查詢數(shù)據(jù)庫的數(shù)據(jù)店量,正確則單元測試成功
4.測試完畢之后芜果,刪除創(chuàng)建的數(shù)據(jù)表

下面來看看實際代碼,如下:

準備用來測試的項目代碼 db_database.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import pymysql
pymysql.install_as_MySQLdb()
from flask_migrate import Migrate,MigrateCommand
from flask_script import Shell,Manager

app = Flask(__name__)
manager = Manager(app)

class Config(object):
    """配置參數(shù)"""
    # 設置連接數(shù)據(jù)庫的URL
    user = 'root'
    password = '***************'
    database = 'flask_ex'
    app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://%s:%s@127.0.0.1:3306/%s' % (user,password,database)

    # 設置sqlalchemy自動更跟蹤數(shù)據(jù)庫
    SQLALCHEMY_TRACK_MODIFICATIONS = True

    # 查詢時會顯示原始SQL語句
    # app.config['SQLALCHEMY_ECHO'] = True

    # 禁止自動提交數(shù)據(jù)處理
    app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = False

    # 設置密鑰融师,用于csrf_token的加解密
    app.config["SECRET_KEY"] = "xhosd6f982yfhowefy29f"

# 讀取配置
app.config.from_object(Config)

# 創(chuàng)建數(shù)據(jù)庫sqlalchemy工具對象
db = SQLAlchemy(app)

#第一個參數(shù)是Flask的實例右钾,第二個參數(shù)是Sqlalchemy數(shù)據(jù)庫實例
migrate = Migrate(app,db)

#manager是Flask-Script的實例,這條語句在flask-Script中添加一個db命令
manager.add_command('db',MigrateCommand)

#定義模型類-作者
class Author(db.Model):
    __tablename__ = 'author'
    id = db.Column(db.Integer,primary_key=True)
    name = db.Column(db.String(32),unique=True)
    email = db.Column(db.String(64))
    au_book = db.relationship('Book',backref='author')
    def __str__(self):
        return 'Author:%s' %self.name


#定義模型類-書名
class Book(db.Model):
    __tablename__ = 'books'
    id = db.Column(db.Integer,primary_key=True)
    info = db.Column(db.String(32),unique=True)
    leader = db.Column(db.String(32))
    au_book = db.Column(db.Integer,db.ForeignKey('author.id'))
    def __str__(self):
        return 'Book:%s,%s'%(self.info,self.leader)


if __name__ == '__main__':

    # 通過管理對象來啟動flask
    manager.run()

進行數(shù)據(jù)庫單元測試的代碼 test_db.py

import unittest
from db_database import app,db,Author,Book
import time

class TestLogin(unittest.TestCase):
    """定義測試案例"""
    def setUp(self):
        """在執(zhí)行具體的測試方法前,先被調用"""

        # 激活測試標志
        app.config['TESTING'] = True

        # 設置用來測試的數(shù)據(jù)庫霹粥,避免使用正式數(shù)據(jù)庫實例[覆蓋原來項目中的數(shù)據(jù)庫配置]
        user = 'root'
        password = '***********'
        # 設置數(shù)據(jù)庫炬丸,測試之前需要創(chuàng)建好 create database testdb charset=utf8;
        database = 'testdb'
        app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://%s:%s@127.0.0.1:3306/%s' % (user, password, database)

        self.app = app

        # 創(chuàng)建數(shù)據(jù)庫的所有模型表:Author漂羊、Book模型表
        db.create_all()

    def tearDown(self):
        # 測試結束操作立润,刪除數(shù)據(jù)庫
        db.session.remove()
        db.drop_all()

    # 測試代碼
    def test_append_data(self):
        au = Author(name='quyuan')
        bk = Book(info='python_book')
        db.session.add_all([au, bk])
        db.session.commit()
        author = Author.query.filter_by(name='quyuan').first()
        book = Book.query.filter_by(info='python_book').first()
        # 斷言數(shù)據(jù)存在
        self.assertIsNotNone(author)
        self.assertIsNotNone(book)
        
        # 休眠10秒幸冻,可以到數(shù)據(jù)庫中查詢表進行確認
        time.sleep(10)


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

測試執(zhí)行,執(zhí)行過程查看mysql的數(shù)據(jù)庫表浩淘,如下:

# 切換數(shù)據(jù)庫testdb
mysql> use testdb;
Database changed
mysql> 
# 查看表為空
mysql> show tables;
Empty set (0.00 sec)

# 執(zhí)行過程捌朴,創(chuàng)建表成功
mysql> show tables;
+------------------+
| Tables_in_testdb |
+------------------+
| author           |
| books            |
+------------------+
2 rows in set (0.00 sec)

mysql> 
# 執(zhí)行完畢,表被全部刪除
mysql> show tables;
Empty set (0.00 sec)

mysql> 

查看單元測試執(zhí)行成功张抄,如下:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末砂蔽,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子署惯,更是在濱河造成了極大的恐慌左驾,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件极谊,死亡現(xiàn)場離奇詭異诡右,居然都是意外死亡,警方通過查閱死者的電腦和手機轻猖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門帆吻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人咙边,你說我怎么就攤上這事猜煮。” “怎么了败许?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵王带,是天一觀的道長。 經(jīng)常有香客問我市殷,道長愕撰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任被丧,我火速辦了婚禮,結果婚禮上绪妹,老公的妹妹穿的比我還像新娘甥桂。我一直安慰自己,他們只是感情好邮旷,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布黄选。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪办陷。 梳的紋絲不亂的頭發(fā)上貌夕,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音民镜,去河邊找鬼啡专。 笑死,一個胖子當著我的面吹牛制圈,可吹牛的內容都是我干的们童。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼鲸鹦,長吁一口氣:“原來是場噩夢啊……” “哼慧库!你這毒婦竟也來了?” 一聲冷哼從身側響起馋嗜,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤齐板,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后葛菇,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體甘磨,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年熟呛,在試婚紗的時候發(fā)現(xiàn)自己被綠了宽档。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡庵朝,死狀恐怖吗冤,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情九府,我是刑警寧澤椎瘟,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站侄旬,受9級特大地震影響肺蔚,放射性物質發(fā)生泄漏。R本人自食惡果不足惜儡羔,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一宣羊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧汰蜘,春花似錦仇冯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春泼舱,著一層夾襖步出監(jiān)牢的瞬間等缀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工娇昙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留尺迂,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓涯贞,卻偏偏與公主長得像枪狂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子宋渔,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內容