欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

在Python的Flask框架中實現(xiàn)單元測試的教程

 更新時間:2015年04月20日 16:25:23   投稿:goldensun  
這篇文章主要介紹了在Python的Flask框架中實現(xiàn)單元測試的教程,屬于自動化部署的方面,可以給debug工作帶來諸多便利,需要的朋友可以參考下

 概要

在前面的章節(jié)里我們專注于在我們的小應(yīng)用程序上一步步的添加功能上。到現(xiàn)在為止我們有了一個帶有數(shù)據(jù)庫的應(yīng)用程序,可以注冊用戶,記錄用戶登陸退出日志以及查看修改配置文件。

在本節(jié)中,我們不為應(yīng)用程序添加任何新功能,相反,我們要尋找一種方法來增加我們已寫代碼的穩(wěn)定性,我們還將創(chuàng)建一個測試框架來幫助我們防止將來程序中出現(xiàn)的失敗和回滾。

讓我們來找bug

在上一章的結(jié)尾談到,我故意在應(yīng)用程序中引入一個bug。接下來讓我描述一下它是什么樣的bug,然后看看當(dāng)我們的程序不按照我們意愿執(zhí)行的時候,它在其中又起了什么樣的影響。

應(yīng)用程序的問題在于,沒有保證用戶昵稱的唯一性。用戶昵稱是由應(yīng)用程序自動初始化的。我們首先會考慮使用OpenID provider給出的用戶的昵稱,然后再考慮使用Email信息中的用戶名部分作為用戶的昵稱。但如果出現(xiàn)重復(fù)的昵稱,則后面的用戶將無法注冊成功。更糟糕的是,在修改用戶配置的表單中,我們允許用戶任意更改他們的昵稱,但我們?nèi)匀粵]有對昵稱沖突進行檢查。

當(dāng)我們分析完錯誤產(chǎn)生時應(yīng)用程序的行為之后,我們將會定位這些問題。

Flask 的調(diào)試功能

那么讓我們看看當(dāng)bug被觸發(fā)時,會出現(xiàn)什么現(xiàn)象。

讓我們從創(chuàng)建一個嶄新的數(shù)據(jù)庫,在linux下,執(zhí)行:
 

rm app.db
./db_create.py

在Windows下,執(zhí)行:
 

del app.db
flask/Scripts/python db_create.py

我們需要兩個OpenID的賬號來重現(xiàn)這個bug。當(dāng)然這兩個賬號最理想的狀態(tài)是來自來個不同的擁有者,那樣可以避免他們的cookie把情況搞的更復(fù)雜。通過如下步驟創(chuàng)建沖突的昵稱:
  •     用第一個賬號登陸
  •     進入用戶信息屬性編輯頁面,將昵稱改為“dup”
  •     登出系統(tǒng)
  •     用第二個賬號登陸
  •     修改第二個賬號的用戶信息屬性,將昵稱改為“dup”


哎喲!sqlalchemy中拋出了一個異常,來看一下錯誤信息:

lalchemy.exc.IntegrityError
IntegrityError: (IntegrityError) column nickname is not unique u'UPDATE user SET nickname=?, about_me=? WHERE user.id = ?' (u'dup', u'', 2)

錯誤的后面是這個錯誤的堆棧信息,事實上,這是一個相當(dāng)不錯的錯誤提示,你可以轉(zhuǎn)向任何框架檢查代碼或者在瀏覽器里執(zhí)行正確的表達式。

這個錯誤信息相當(dāng)明確,我們試圖在數(shù)據(jù)插入一個重復(fù)的昵稱,數(shù)據(jù)庫的昵稱字段是一個衛(wèi)衣鍵,因此這樣的操作是無效的。
 

除了實際的錯誤,在我們手頭上還有一個次要的錯誤。如果一個用戶不注意在我們應(yīng)用程序里引起了一個錯誤(這一個錯誤或者任何其他原因引起的異常),應(yīng)用程序?qū)⑾蛩?她暴漏錯誤信息和堆棧信息,而不是暴露給我們。對于我們開發(fā)者來說這是個很好的特性,但是很多時候我們不想讓用戶看到這些信息。

這么長時間以來,我們一直在debug模式下運行我們的應(yīng)用程序,我們通過設(shè)置debug=True的參數(shù)來啟用應(yīng)用程序的debug模式。這里我們在運行腳本run.py里配置。

當(dāng)我們這樣開發(fā)應(yīng)用是方便的,但是我們需要在生產(chǎn)環(huán)境上關(guān)閉debug模式。 讓我們創(chuàng)建另一個啟動腳本文件設(shè)置關(guān)閉dubug模式(filerunp.py): 
 

#!flask/bin/python
from app import app
app.run(debug = False)

現(xiàn)在重新啟動應(yīng)用:

./runp.py

并且現(xiàn)在再嘗試重命名第二個賬號nickname成‘dup'

這次我們沒有獲取到一個錯誤信息,取而代之,我們得到了一個HTTP 500錯誤碼,這是個內(nèi)部服務(wù)器錯誤。雖然這不容易定位錯誤,但至少沒有暴露我們應(yīng)用程序的任何細(xì)節(jié)給陌生人。當(dāng)調(diào)試關(guān)閉后出現(xiàn)一個異常時,F(xiàn)lask會產(chǎn)生一個500頁面。

雖然這樣好些了,但現(xiàn)在仍存在兩個問題。首先美化問題:默認(rèn)的500頁面很丑陋。第二個問題更重要些,當(dāng)用戶操作失敗時,我們無法獲取到錯誤信息了,因為錯誤在后臺默默的處理了。幸運的是有個簡單方式來處理這兩個問題。

定制HTTP錯誤處理程序

Flask為應(yīng)用程序提供了一個機制來安裝他們自己的錯誤頁面,作為例子,讓我們定義兩個最常見的HTTP 404和500錯誤的自定義頁面。定制其他錯誤頁面也是同樣的方式。

使用一個修飾來聲明一個定制的錯誤處理程序 (fileapp/views.py):
 

@app.errorhandler(404)
def internal_error(error):
  return render_template('404.html'), 404
 
@app.errorhandler(500)
def internal_error(error):
  db.session.rollback()
  return render_template('500.html'), 500

這地方無需多言,因為他們都是不言而喻的。唯一有趣的地方時錯誤500處理中的rollack語句,這個地方是不可缺少的因為這個方法會被當(dāng)做一個異常調(diào)用。如果因為數(shù)據(jù)庫錯誤導(dǎo)致一個異常,那么數(shù)據(jù)庫的會話將變成一個無效狀態(tài),因此我們需要回滾它,以防止一個會話轉(zhuǎn)向一個500錯誤的模板。

這是一個404錯誤在模版
 

<!-- extend base layout -->
{% extends "base.html" %}
 
{% block content %}
<h1>File Not Found</h1>
<p><a href="{{url_for('index')}}">Back</a></p>
{% endblock %}

這是一個500錯誤的模版
 

<!-- extend base layout -->
{% extends "base.html" %}
 
{% block content %}
<h1>An unexpected error has occurred</h1>
<p>The administrator has been notified. Sorry for the inconvenience!</p>
<p><a href="{{url_for('index')}}">Back</a></p>
{% endblock %}

注意,我們會繼續(xù)使用我們base.html 布局, 這樣我們的錯誤頁看起來比較舒服

通過email發(fā)送錯誤日志

為了處理第二個問題我們需要配置應(yīng)用的錯誤報告機制。

第一個是每當(dāng)有錯誤發(fā)生時把錯誤日志通過郵件發(fā)送給我們。

首先,我們需要在我們的應(yīng)用配置郵件服務(wù)器和管理員列表 (fileconfig.py):
 

# mail server settings
MAIL_SERVER = 'localhost'
MAIL_PORT = 25
MAIL_USERNAME = None
MAIL_PASSWORD = None
 
# administrator list
ADMINS = ['you@example.com']

當(dāng)然,你要把上面的配置改成你自己的才有意義
 

Flask 使用通用的Python logging模塊, 所以設(shè)置發(fā)送錯誤日志郵件非常簡單. (fileapp/__init__.py):
 

from config import basedir, ADMINS, MAIL_SERVER, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD
 
if not app.debug:
  import logging
  from logging.handlers import SMTPHandler
  credentials = None
  if MAIL_USERNAME or MAIL_PASSWORD:
    credentials = (MAIL_USERNAME, MAIL_PASSWORD)
  mail_handler = SMTPHandler((MAIL_SERVER, MAIL_PORT), 'no-reply@' + MAIL_SERVER, ADMINS, 'microblog failure', credentials)
  mail_handler.setLevel(logging.ERROR)
  app.logger.addHandler(mail_handler)

Note that we are only enabling the emails when we run without debugging.
注意,我們要的非dubug模式下開啟郵件功能.
 

在沒有郵件服務(wù)器的pc上測試郵件功能也很容易,幸好Python有SMTP的測試排錯的服務(wù)器(SMTP debugging server)。打開一個控制臺窗口,并且運行下面的命令:
 

python -m smtpd -n -c DebuggingServer localhost:25

當(dāng)程序運行的時候,應(yīng)用接收和發(fā)送郵件會在控制臺窗口中顯示出來。

打印日志到文件

通過郵件接收錯誤日志非常不錯,但是,這是不夠的。有些導(dǎo)致失敗的條件不會觸發(fā)異常并且不是主要的問題,所以我們需要將日志保存到log文件中,在某些情況下,需要日志來進行排錯。

出于這個原因,我們的應(yīng)用需要一個日志文件。

開啟文件日志和郵件日志很相似(fileapp/__init__.py):
 

if not app.debug:
  import logging
  from logging.handlers import RotatingFileHandler
  file_handler = RotatingFileHandler('tmp/microblog.log', 'a', 1 * 1024 * 1024, 10)
  file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
  app.logger.setLevel(logging.INFO)
  file_handler.setLevel(logging.INFO)
  app.logger.addHandler(file_handler)
  app.logger.info('microblog startup')


日志文件將在tmp目錄下生成,文件名叫microblog.log。我們使用的RotatingFileHandler方法中有個限制日志數(shù)量的參數(shù)。在這種情況下,我們限制了一個日志文件大小為1M,并且把最后的十個文件作為備份。
logging.Formatter類提供了日志信息的定義格式,由于這些信息將寫到一個文件中,我們想獲取到盡可能多的信息,因此除了日志信息和堆棧信息,我們還寫了個時間戳,日志級別和文件名、信息的行號。


為了使日志更有用,我們降低了應(yīng)用程序日志和文件日志處理程序的日志級別,因為這樣我們將有機會在沒有錯誤情況下把有用信息寫入到日志中。作為一個例子,我們啟動時將日志的級別設(shè)置為信息級別。從現(xiàn)在開始,每次你啟動應(yīng)用程序?qū)⒂涗浤愕恼{(diào)試信息。

當(dāng)我們沒有使用日志時,調(diào)試一個在線和使用中的web服務(wù)時是件非常困難的事,把日志信息寫入到文件中,將是我們診斷和解決問題的一個有用工具,所以現(xiàn)在讓我們準(zhǔn)備好使用這個功能吧。

bug修復(fù)

讓我們來修復(fù)下昵稱重復(fù)的bug.

前面討論過,有兩個地方目前還沒有處理重復(fù)。首先是Flask-Login的after_login處理,這個方法將在用戶成功登陸到系統(tǒng)后調(diào)用,我們需要創(chuàng)建一個新的User實例。這是受影響的一個代碼片段,我們做了修復(fù) (fileapp/views.py):
 

if user is None:
    nickname = resp.nickname
    if nickname is None or nickname == "":
      nickname = resp.email.split('@')[0]
    nickname = User.make_unique_nickname(nickname)
    user = User(nickname = nickname, email = resp.email, role = ROLE_USER)
    db.session.add(user)
    db.session.commit()

 
我們解決這個問題的方法是讓User類選擇一個唯一的名字給我們,這也是 make_unique_nickname方法所做的(fileapp/models.py):
 

class User(db.Model):
  # ...
  @staticmethod
  def make_unique_nickname(nickname):
    if User.query.filter_by(nickname = nickname).first() == None:
      return nickname
    version = 2
    while True:
      new_nickname = nickname + str(version)
      if User.query.filter_by(nickname = new_nickname).first() == None:
        break
      version += 1
    return new_nickname
  # ...

這個方法簡單的添加一個計數(shù)器來生成一個唯一的昵稱名。例如,如果用戶名“miguel”存在,這個方法將建議你使用“miguel2”,但是如果它也存在就會生成“miguel3”···。注意我們把這個方法設(shè)定為靜態(tài)方法,因為這個操作不適用于任何類的實例。

第二個導(dǎo)致重復(fù)昵稱的地方是編輯頁面視圖函數(shù),這算是用戶選擇昵稱的一個小惡作劇,正確的方式是不允許用戶輸入重復(fù)名稱,讓用戶更換為另一個名稱。我們通過添加form表單驗證來解決這個問題,如果用戶輸入一個無效的昵稱,將會得到一個字段驗證失敗信息,添加我們的驗證只需重寫form的validate方法 (fileapp/forms.py):

 
class EditForm(Form):
  nickname = TextField('nickname', validators = [Required()])
  about_me = TextAreaField('about_me', validators = [Length(min = 0, max = 140)])
 
  def __init__(self, original_nickname, *args, **kwargs):
    Form.__init__(self, *args, **kwargs)
    self.original_nickname = original_nickname
 
  def validate(self):
    if not Form.validate(self):
      return False
    if self.nickname.data == self.original_nickname:
      return True
    user = User.query.filter_by(nickname = self.nickname.data).first()
    if user != None:
      self.nickname.errors.append('This nickname is already in use. Please choose another one.')
      return False
    return True

 

表單的構(gòu)造函數(shù)增加了一個新的參數(shù)original_nickname,驗證方法validate使用這個參數(shù)來判斷昵稱是否修改了,如果沒有修改就直接返回它,如果已經(jīng)修改了,方法會確認(rèn)下新的昵稱在數(shù)據(jù)庫是否已經(jīng)存在。

接下來我們在視圖函數(shù)中添加新的構(gòu)造器參數(shù):
 

@app.route('/edit', methods = ['GET', 'POST'])
@login_required
def edit():
  form = EditForm(g.user.nickname)
  # ...

完成這個修改我們還必須在表單的模板中啟用錯誤顯示字段 (文件app/templates/edit.html):

 

<td>Your nickname:</td>
    <td>
      {{form.nickname(size = 24)}}
      {% for error in form.errors.nickname %}
      <br><span style="color: red;">[{{error}}]</span>
      {% endfor %}
    </td>

現(xiàn)在這個bug已經(jīng)修復(fù)了,阻止了重復(fù)數(shù)據(jù)的出現(xiàn)···除非這些驗證方法不能正常工作了。在兩個或者多個線程/進程并行存取數(shù)據(jù)庫時,這仍然存在一個潛在的問題,但這些都是以后我們文章討論的主題。

在這里你可以嘗試選擇一個重復(fù)的名稱來看看表單如何處理這些錯誤的。
 
單元測試框架

先把上面關(guān)于測試的會話放一下,咱們來討論下關(guān)于自動化測試的話題。

隨著應(yīng)用程序規(guī)模的增長,越來越難以確定代碼的改變是否會影響到現(xiàn)有的功能。

傳統(tǒng)的方法防止回歸是一個很好的方式,你通過編寫單元測試來測試應(yīng)用程序所有不同功能,每一個測試集中于一個點來驗證結(jié)果是否和預(yù)期的一致。測試程序通過定期的執(zhí)行來確認(rèn)應(yīng)用程序是否在正常工作。當(dāng)測試覆蓋率變大時,你就可以自信的修改和添加新功能,只需通過測試程序來驗證下是否影響到了應(yīng)用程序現(xiàn)有功能。


現(xiàn)在我們使用python的unittest測試組件來創(chuàng)建個簡單的測試框架 (tests.py):
 

#!flask/bin/python
import unittest
 
from config import basedir
from app import app, db
from app.models import User
 
class TestCase(unittest.TestCase):
  def setUp(self):
    app.config['TESTING'] = True
    app.config['CSRF_ENABLED'] = False
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'test.db')
    self.app = app.test_client()
    db.create_all()
 
  def tearDown(self):
    db.session.remove()
    db.drop_all()
 
  def test_avatar(self):
    u = User(nickname = 'john', email = 'john@example.com')
    avatar = u.avatar(128)
    expected = 'http://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6'
    assert avatar[0:len(expected)] == expected
 
  def test_make_unique_nickname(self):
    u = User(nickname = 'john', email = 'john@example.com')
    db.session.add(u)
    db.session.commit()
    nickname = User.make_unique_nickname('john')
    assert nickname != 'john'
    u = User(nickname = nickname, email = 'susan@example.com')
    db.session.add(u)
    db.session.commit()
    nickname2 = User.make_unique_nickname('john')
    assert nickname2 != 'john'
    assert nickname2 != nickname
 
if __name__ == '__main__':
  unittest.main()

unittest測試組件的討論超出了本文的范圍了,我們這里只需知道TestCase是我們的測試類。setUp和tearDown方法有些特殊,它們分別在每個測試方法前后執(zhí)行,復(fù)雜點的設(shè)置可以包含幾組測試,每個代表一個單元測試,TestCase的子類和每個組都將擁有獨立的setUp和tearDown方法。


這些特殊的setUp和tearDown方法都是非常通用的,在setUp可以方便的修改配置,例如,我們想測試不同的數(shù)據(jù)庫作為主數(shù)據(jù)庫,在tearDown里面只需簡單設(shè)置下數(shù)據(jù)庫內(nèi)容就可以。

測試作為方法被實現(xiàn),一個測試應(yīng)該運行一些已知結(jié)果的應(yīng)用程序方法,也應(yīng)當(dāng)能夠斷言出結(jié)果和預(yù)期的不同。


到目前為止,在我們的測試框架里有兩個測試。第一個驗證來自于上一篇文章的Gravatar avatar URLs生成的是否正確,注意預(yù)期的avatar被硬編碼在測試中,和User類中返回的對象作比較。

第二個測試驗證是test_make_unique_nickname方法,同樣也是在User類中。這個測試有點詳細(xì),它創(chuàng)建了一個新的用戶并且寫入數(shù)據(jù)庫中,同時確定名字的唯一性。接下來創(chuàng)建第二個用戶,建議使用唯一名稱,你可以嘗試下使用第一個用戶名稱。在第二部分測試預(yù)期結(jié)果是建議使用與之前不同的名稱。


運行這個測試套件你只需運行tests.py腳本:
 

./tests.py

如果出現(xiàn)錯誤信息,你將會在控制臺得到一個報告。
結(jié)語

今天關(guān)于調(diào)試,錯誤和測試的討論到此為止,我希望這篇文章能對你有用。

老規(guī)矩,如果你有任何評論請寫在下面.

微博應(yīng)用程序的代碼今天修改的更新,你可以在這里下載:


下載 microblog-0.7.zip.

相關(guān)文章

  • Python使用5行代碼批量做小姐姐的素描圖

    Python使用5行代碼批量做小姐姐的素描圖

    本文主要介紹了Python使用5行代碼批量做小姐姐的素描圖,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-07-07
  • Python多線程threading模塊用法實例分析

    Python多線程threading模塊用法實例分析

    這篇文章主要介紹了Python多線程threading模塊用法,結(jié)合實例形式分析了Python多線程threading模塊原理、功能、常見應(yīng)用及相關(guān)操作注意事項,需要的朋友可以參考下
    2019-05-05
  • 解決python gdal投影坐標(biāo)系轉(zhuǎn)換的問題

    解決python gdal投影坐標(biāo)系轉(zhuǎn)換的問題

    今天小編就為大家分享一篇解決python gdal投影坐標(biāo)系轉(zhuǎn)換的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-01-01
  • Pandas 內(nèi)置的十種畫圖方法

    Pandas 內(nèi)置的十種畫圖方法

    這篇文章主要介紹了Pandas 內(nèi)置的十種畫圖方法,Pandas是非常常見的數(shù)據(jù)分析工具,我們一般都會處理好處理數(shù)據(jù)然后使用searbon或matplotlib來進行繪制
    2022-09-09
  • Python基于回溯法解決01背包問題實例

    Python基于回溯法解決01背包問題實例

    這篇文章主要介紹了Python基于回溯法解決01背包問題,結(jié)合實例形式分析了Python回溯法采用深度優(yōu)先策略搜索解決01背包問題的相關(guān)操作技巧,需要的朋友可以參考下
    2017-12-12
  • python之Socket網(wǎng)絡(luò)編程詳解

    python之Socket網(wǎng)絡(luò)編程詳解

    這篇文章主要為大家詳細(xì)介紹了python之Socket網(wǎng)絡(luò)編程,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2016-09-09
  • Python腳本修改阿里云的訪問控制列表的方法

    Python腳本修改阿里云的訪問控制列表的方法

    這篇文章主要介紹了Python腳本修改阿里云的訪問控制列表的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2019-03-03
  • Python提取PDF發(fā)票信息保存Excel文件并制作EXE程序的全過程

    Python提取PDF發(fā)票信息保存Excel文件并制作EXE程序的全過程

    之前零散的用過一點python做數(shù)據(jù)處理,這次又遇到一個數(shù)據(jù)處理的小功能,下面這篇文章主要給大家介紹了關(guān)于Python提取PDF發(fā)票信息保存Excel文件并制作EXE程序的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-11-11
  • pytorch中的torch.nn.Conv2d()函數(shù)圖文詳解

    pytorch中的torch.nn.Conv2d()函數(shù)圖文詳解

    這篇文章主要給大家介紹了關(guān)于pytorch中torch.nn.Conv2d()函數(shù)的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2022-02-02
  • python保留小數(shù)函數(shù)的幾種使用總結(jié)

    python保留小數(shù)函數(shù)的幾種使用總結(jié)

    本文主要介紹了python保留小數(shù)函數(shù)的幾種使用總結(jié),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-02-02

最新評論