概要
在前面的章节里我们专注于在我们的小应用程序上一步步的添加功能上。到现在为止我们有了一个带有数据库的应用程序,可以注册用户,记录用户登陆退出日志以及查看修改配置文件。
在本节中,我们不为应用程序添加任何新功能,相反,我们要寻找一种方法来增加我们已写代码的稳定性,我们还将创建一个测试框架来帮助我们防止将来程序中出现的失败和回滚。
让我们来找bug
在上一章的结尾谈到,我故意在应用程序中引入一个bug。接下来让我描述一下它是什么样的bug,然后看看当我们的程序不按照我们意愿执行的时候,它在其中又起了什么样的影响。
应用程序的问题在于,没有保证用户昵称的唯一性。用户昵称是由应用程序自动初始化的。我们首先会考虑使用OpenID provider给出的用户的昵称,然后再考虑使用Email信息中的用户名部分作为用户的昵称。但如果出现重复的昵称,则后面的用户将无法注册成功。更糟糕的是,在修改用户配置的表单中,我们允许用户任意更改他们的昵称,但我们仍然没有对昵称冲突进行检查。
当我们分析完错误产生时应用程序的行为之后,我们将会定位这些问题。
Flask 的调试功能
那么让我们看看当bug被触发时,会出现什么现象。
让我们从创建一个崭新的数据库,在linux下,执行:
rm app.db ./db_create.py
在Windows下,执行:
del app.db flask/Scripts/python db_create.py
- 用第一个账号登陆
- 进入用户信息属性编辑页面,将昵称改为“dup”
- 登出系统
- 用第二个账号登陆
- 修改第二个账号的用户信息属性,将昵称改为“dup”
哎哟!sqlalchemy中抛出了一个异常,来看一下错误信息:
lalchemy.exc.IntegrityError IntegrityError: (IntegrityError) column nickname is not unique u'UPDATE user SET nickname="htmlcode">#!flask/bin/python from app import app app.run(debug = False)现在重新启动应用:
./runp.py并且现在再尝试重命名第二个账号nickname成‘dup'
这次我们没有获取到一个错误信息,取而代之,我们得到了一个HTTP 500错误码,这是个内部服务器错误。虽然这不容易定位错误,但至少没有暴露我们应用程序的任何细节给陌生人。当调试关闭后出现一个异常时,Flask会产生一个500页面。
虽然这样好些了,但现在仍存在两个问题。首先美化问题:默认的500页面很丑陋。第二个问题更重要些,当用户操作失败时,我们无法获取到错误信息了,因为错误在后台默默的处理了。幸运的是有个简单方式来处理这两个问题。
定制HTTP错误处理程序
Flask为应用程序提供了一个机制来安装他们自己的错误页面,作为例子,让我们定义两个最常见的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语句,这个地方是不可缺少的因为这个方法会被当做一个异常调用。如果因为数据库错误导致一个异常,那么数据库的会话将变成一个无效状态,因此我们需要回滚它,以防止一个会话转向一个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 %}注意,我们会继续使用我们base.html 布局, 这样我们的错误页看起来比较舒服
通过email发送错误日志
为了处理第二个问题我们需要配置应用的错误报告机制。
第一个是每当有错误发生时把错误日志通过邮件发送给我们。
首先,我们需要在我们的应用配置邮件服务器和管理员列表 (fileconfig.py):
# mail server settings MAIL_SERVER = 'localhost' MAIL_PORT = 25 MAIL_USERNAME = None MAIL_PASSWORD = None # administrator list ADMINS = ['you@example.com']当然,你要把上面的配置改成你自己的才有意义
Flask 使用通用的Python logging模块, 所以设置发送错误日志邮件非常简单. (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模式下开启邮件功能.
在没有邮件服务器的pc上测试邮件功能也很容易,幸好Python有SMTP的测试排错的服务器(SMTP debugging server)。打开一个控制台窗口,并且运行下面的命令:
python -m smtpd -n -c DebuggingServer localhost:25当程序运行的时候,应用接收和发送邮件会在控制台窗口中显示出来。
打印日志到文件
通过邮件接收错误日志非常不错,但是,这是不够的。有些导致失败的条件不会触发异常并且不是主要的问题,所以我们需要将日志保存到log文件中,在某些情况下,需要日志来进行排错。
出于这个原因,我们的应用需要一个日志文件。
开启文件日志和邮件日志很相似(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方法中有个限制日志数量的参数。在这种情况下,我们限制了一个日志文件大小为1M,并且把最后的十个文件作为备份。
logging.Formatter类提供了日志信息的定义格式,由于这些信息将写到一个文件中,我们想获取到尽可能多的信息,因此除了日志信息和堆栈信息,我们还写了个时间戳,日志级别和文件名、信息的行号。
为了使日志更有用,我们降低了应用程序日志和文件日志处理程序的日志级别,因为这样我们将有机会在没有错误情况下把有用信息写入到日志中。作为一个例子,我们启动时将日志的级别设置为信息级别。从现在开始,每次你启动应用程序将记录你的调试信息。当我们没有使用日志时,调试一个在线和使用中的web服务时是件非常困难的事,把日志信息写入到文件中,将是我们诊断和解决问题的一个有用工具,所以现在让我们准备好使用这个功能吧。
bug修复
让我们来修复下昵称重复的bug.
前面讨论过,有两个地方目前还没有处理重复。首先是Flask-Login的after_login处理,这个方法将在用户成功登陆到系统后调用,我们需要创建一个新的User实例。这是受影响的一个代码片段,我们做了修复 (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 # ...这个方法简单的添加一个计数器来生成一个唯一的昵称名。例如,如果用户名“miguel”存在,这个方法将建议你使用“miguel2”,但是如果它也存在就会生成“miguel3”···。注意我们把这个方法设定为静态方法,因为这个操作不适用于任何类的实例。
第二个导致重复昵称的地方是编辑页面视图函数,这算是用户选择昵称的一个小恶作剧,正确的方式是不允许用户输入重复名称,让用户更换为另一个名称。我们通过添加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
表单的构造函数增加了一个新的参数original_nickname,验证方法validate使用这个参数来判断昵称是否修改了,如果没有修改就直接返回它,如果已经修改了,方法会确认下新的昵称在数据库是否已经存在。
接下来我们在视图函数中添加新的构造器参数:
@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>现在这个bug已经修复了,阻止了重复数据的出现···除非这些验证方法不能正常工作了。在两个或者多个线程/进程并行存取数据库时,这仍然存在一个潜在的问题,但这些都是以后我们文章讨论的主题。
在这里你可以尝试选择一个重复的名称来看看表单如何处理这些错误的。
单元测试框架先把上面关于测试的会话放一下,咱们来讨论下关于自动化测试的话题。
随着应用程序规模的增长,越来越难以确定代码的改变是否会影响到现有的功能。
传统的方法防止回归是一个很好的方式,你通过编写单元测试来测试应用程序所有不同功能,每一个测试集中于一个点来验证结果是否和预期的一致。测试程序通过定期的执行来确认应用程序是否在正常工作。当测试覆盖率变大时,你就可以自信的修改和添加新功能,只需通过测试程序来验证下是否影响到了应用程序现有功能。
现在我们使用python的unittest测试组件来创建个简单的测试框架 (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方法有些特殊,它们分别在每个测试方法前后执行,复杂点的设置可以包含几组测试,每个代表一个单元测试,TestCase的子类和每个组都将拥有独立的setUp和tearDown方法。
这些特殊的setUp和tearDown方法都是非常通用的,在setUp可以方便的修改配置,例如,我们想测试不同的数据库作为主数据库,在tearDown里面只需简单设置下数据库内容就可以。测试作为方法被实现,一个测试应该运行一些已知结果的应用程序方法,也应当能够断言出结果和预期的不同。
到目前为止,在我们的测试框架里有两个测试。第一个验证来自于上一篇文章的Gravatar avatar URLs生成的是否正确,注意预期的avatar被硬编码在测试中,和User类中返回的对象作比较。第二个测试验证是test_make_unique_nickname方法,同样也是在User类中。这个测试有点详细,它创建了一个新的用户并且写入数据库中,同时确定名字的唯一性。接下来创建第二个用户,建议使用唯一名称,你可以尝试下使用第一个用户名称。在第二部分测试预期结果是建议使用与之前不同的名称。
运行这个测试套件你只需运行tests.py脚本:
./tests.py如果出现错误信息,你将会在控制台得到一个报告。
结语今天关于调试,错误和测试的讨论到此为止,我希望这篇文章能对你有用。
老规矩,如果你有任何评论请写在下面.
微博应用程序的代码今天修改的更新,你可以在这里下载:
下载 microblog-0.7.zip.
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。
更新日志
- 凤飞飞《我们的主题曲》飞跃制作[正版原抓WAV+CUE]
- 刘嘉亮《亮情歌2》[WAV+CUE][1G]
- 红馆40·谭咏麟《歌者恋歌浓情30年演唱会》3CD[低速原抓WAV+CUE][1.8G]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[320K/MP3][193.25MB]
- 【轻音乐】曼托凡尼乐团《精选辑》2CD.1998[FLAC+CUE整轨]
- 邝美云《心中有爱》1989年香港DMIJP版1MTO东芝首版[WAV+CUE]
- 群星《情叹-发烧女声DSD》天籁女声发烧碟[WAV+CUE]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[FLAC/分轨][748.03MB]
- 理想混蛋《Origin Sessions》[320K/MP3][37.47MB]
- 公馆青少年《我其实一点都不酷》[320K/MP3][78.78MB]
- 群星《情叹-发烧男声DSD》最值得珍藏的完美男声[WAV+CUE]
- 群星《国韵飘香·贵妃醉酒HQCD黑胶王》2CD[WAV]
- 卫兰《DAUGHTER》【低速原抓WAV+CUE】
- 公馆青少年《我其实一点都不酷》[FLAC/分轨][398.22MB]
- ZWEI《迟暮的花 (Explicit)》[320K/MP3][57.16MB]