隨著專案規模增長,手動測試已無法覆蓋所有情境。本文示範如何在 Flask 專案中建立單元測試框架、客製化 HTTP 錯誤頁面、設定錯誤日誌郵寄與檔案記錄,並修復一個昵稱唯一性的實際 Bug。
原始碼在 OpenID 回調與編輯資料表單皆未檢查昵稱重複,導致資料庫拋出 IntegrityError。在開發模式下會顯示堆疊追蹤,生產模式則回傳 500 錯誤頁面。
建立 runp.py 關閉 debug:
#!flask/bin/python
from app import app
app.run(debug=False)
在 app/views.py 註冊錯誤處理器:
@app.errorhandler(404)
def not_found_error(error):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('500.html'), 500
對應模板 404.html、500.html 繼承 base.html,提供友善提示與返回連結。
在 config.py 設定郵件伺服器與管理員:
MAIL_SERVER = 'localhost'
MAIL_PORT = 25
ADMINS = ['admin@example.com']
在 app/__init__.py 初始化 SMTPHandler 與 RotatingFileHandler:
if not app.debug:
import logging
from logging.handlers import SMTPHandler, RotatingFileHandler
# 郵件錯誤通知
credentials = (MAIL_USERNAME, MAIL_PASSWORD) if MAIL_USERNAME or MAIL_PASSWORD else None
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)
# 檔案日誌
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')
@staticmethod
def make_unique_nickname(nickname):
if User.query.filter_by(nickname=nickname).first() is None:
return nickname
version = 2
while True:
new_nickname = nickname + str(version)
if User.query.filter_by(nickname=new_nickname).first() is None:
break
version += 1
return new_nickname
在 after_login 中呼叫:
nickname = User.make_unique_nickname(nickname)
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 is not None:
self.nickname.errors.append('This nickname is already in use. Please choose another one.')
return False
return True
視圖函數傳入原始昵稱:form = EditForm(g.user.nickname)。模板顯示錯誤訊息。
使用 Python 內建 unittest,在 tests.py 撰寫測試案例:
import unittest
import os
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 = 'https://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6'
assert avatar[: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'
u2 = User(nickname=nickname, email='susan@example.com')
db.session.add(u2)
db.session.commit()
nickname2 = User.make_unique_nickname('john')
assert nickname2 != 'john'
assert nickname2 != nickname
if __name__ == '__main__':
unittest.main()
執行 ./tests.py 即可跑測試套件。測試涵蓋 Gravatar 網址生成與昵稱唯一化邏輯,確保重構後功能正常。