我们现在可以试着在控制台向数据库添加一个用户:

In[2]: import model;
In[3]: from microblog import db;
In[4]: u=model.User(nickname="wll",email="wll@mail.com");
In[5]: db.session.add(u);
In[6]: db.session.commit();

接下来我们运行程序,我们将使用以前的用户登录,登录之后将他的用户名也改为wll,看程序运行后会出现什么错误:

    

这是我们会发现浏览器页面上只是报服务器内部错误。我们没办法具体知道程序哪里出现了错误。这是我们在开发过程中不希望看到的。这时我们可以试着让我们的应用程序以调试模式运行。调试模式是在应用程序运行的时候通过在 run 方法中传入参数 debug = True

现在我们继续运行程序:(将用户名改为与前面添加的用户相同的名字)

我们可以看到不会再报服务器内部错误,而是显示具体的错误信息:

从这条信息我们很明显可以看到是违反了唯一约束。

但是我们不希望我们的用户能够看的到程序内部错误,我们只有把debug设置为False。但是现在又有两个问题出现了:第一个是外观上的:默认的 500 错误页很丑陋。第二个小问题相当重要。我们可能不会知道什么时候用户会在我们的程序中会遇到一个失败因为现在调试被禁用。幸好有两种简单的方式解决这两个问题。

一、定制HTTP错误处理器:

Flask 为应用程序提供了一种安装自己的错误页的机制。作为例子,让我们自定义 HTTP 404 以及 500 错误页,这是最常见的两个。定义其它错误的方式是一样的。为了声明一个定制的错误处理器,需要使用装饰器 errorhandler (文件microblog.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 处理器中的 rollback 声明。这是很有必要的因为这个函数是被作为异常的结果被调用。如果异常是被一个数据库错误触发,数据库的会话会处于一个不正常的状态,因此我们必须把会话回滚到正常工作状态在渲染 500 错误页模板之前。

404错误模板:(templates/404.html)

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

500错误模板:(templates/500.html)

{% 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 %}

二、通过电子邮件发送错误

为了解决我们第二个问题,我们将会配置两种应用程序错误报告机制。第一个就是当错误发生的时候发送电子邮件。

在开始之前我们先在应用程序中配置邮件服务器以及管理员邮箱地址(文件 config.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 模块,因此当发生异常的时候发送邮件是十分简单(microblog.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)

在一个没有邮件服务器的开发机器上测试上述代码是相当容易的,多亏了 Python 的 SMTP 调试服务器。仅需要打开一个新的命令行窗口(Windows 用户打开命令提示符)接着运行如下内容打开一个伪造的邮箱服务器:

我们将程序中的调试模式关闭(debug=False),我们将会在命令提示符中看到具体的错误:(如下图所示:)

三、记录到文件

通过邮件接收错误是不错的,但是有时候这并不够。有些失败并不是结束于异常而且也不是主要问题,然而我们可能想要在日志中追踪它们以便做一些调试。

出于这个原因,我们还要为应用程序保持一个日志文件。

启用日志记录类似于电子邮件发送错误(文件microblog.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 以至于生成的日志的大小是有限制的。在这个例子中,我们的日志文件的大小限制在 1 兆,我们将保留最后 10 个日志文件作为备份。

logging.Formatter 类能够定制化日志信息的格式。由于这些信息记录到一个文件中,我们希望它们提供尽可能多的信息,所以我们写一个时间戳,日志记录级别和消息起源于以及日志消息和堆栈跟踪的文件和行号。

为了使得日志更有作用,我们降低了应用程序日志以及文件日志处理器的级别,这样给我们机会写入有用的信息到日志并不是必须错误发生的时候。从这以后,每次你以非调试模式启动有用程序,日志将会记录事件。

虽然我们不会在这个时候有很多记录器的需求,调试的一个处于联机状态并在使用中的网页服务器是非常困难的。消息记录到一个文件,是一个非常有用的工具,在诊断和定位问题,所以我们现在都准备好,我们需要使用此功能。

下面我们来测试下,我们还是编辑相同的错误,将用户名改为数据库已存在用户的名字,我们可以看到tmp文件夹下多了一个microblog.log的文件,文件内容如下:

2016-11-01 11:06:27,548 INFO: microblog startup [in C:\Users\wls003\PycharmProjects\microblog_study\microblog.py:30]
2016-11-01 11:06:27,595 INFO: microblog startup [in microblog.py:30]
2016-11-01 11:06:51,341 ERROR: Exception on /edit [POST] [in C:\Users\wls003\Anaconda2\lib\site-packages\flask\app.py:1423]
Traceback (most recent call last):
File "C:\Users\wls003\Anaconda2\lib\site-packages\flask\app.py", line 1817, in wsgi_app
response = self.full_dispatch_request()
File "C:\Users\wls003\Anaconda2\lib\site-packages\flask\app.py", line 1477, in full_dispatch_request
rv = self.handle_user_exception(e)
File "C:\Users\wls003\Anaconda2\lib\site-packages\flask\app.py", line 1381, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "C:\Users\wls003\Anaconda2\lib\site-packages\flask\app.py", line 1475, in full_dispatch_request
rv = self.dispatch_request()
File "C:\Users\wls003\Anaconda2\lib\site-packages\flask\app.py", line 1461, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "C:\Users\wls003\Anaconda2\lib\site-packages\flask_login.py", line 758, in decorated_view
return func(*args, **kwargs)
File "microblog.py", line 122, in edit
db.session.commit()
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\orm\scoping.py", line 157, in do
return getattr(self.registry(), name)(*args, **kwargs)
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\orm\session.py", line 801, in commit
self.transaction.commit()
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\orm\session.py", line 392, in commit
self._prepare_impl()
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\orm\session.py", line 372, in _prepare_impl
self.session.flush()
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\orm\session.py", line 2019, in flush
self._flush(objects)
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\orm\session.py", line 2137, in _flush
transaction.rollback(_capture_exception=True)
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\util\langhelpers.py", line 60, in __exit__
compat.reraise(exc_type, exc_value, exc_tb)
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\orm\session.py", line 2101, in _flush
flush_context.execute()
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 373, in execute
rec.execute(self)
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 532, in execute
uow
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\orm\persistence.py", line 170, in save_obj
mapper, table, update)
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\orm\persistence.py", line 706, in _emit_update_statements
execute(statement, multiparams)
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\engine\base.py", line 914, in execute
return meth(self, multiparams, params)
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\sql\elements.py", line 323, in _execute_on_connection
return connection._execute_clauseelement(self, multiparams, params)
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\engine\base.py", line 1010, in _execute_clauseelement
compiled_sql, distilled_params
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\engine\base.py", line 1146, in _execute_context
context)
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\engine\base.py", line 1341, in _handle_dbapi_exception
exc_info
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\util\compat.py", line 200, in raise_from_cause
reraise(type(exception), exception, tb=exc_tb, cause=cause)
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\engine\base.py", line 1139, in _execute_context
context)
File "C:\Users\wls003\Anaconda2\lib\site-packages\sqlalchemy\engine\default.py", line 450, in do_execute
cursor.execute(statement, parameters)
IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: user.nickname [SQL: u'UPDATE user SET nickname=?, about_me=? WHERE user.id = ?'] [parameters: (u'wll', u'hello,my name is ninicwang!!!', 1)]

四、修复bug

现在让我们解决 nickname 重复的问题。

像之前讨论的,目前存在两个地方没有处理重复。第一个就是在 after_login 函数。当一个用户成功地登录进系统这个函数就会被调用,这里我们需要创建一个新的 User 实例。这里就是受影响的代码块(文件microblog.py):

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

解决问题的方式就是让 User 类为我们选择一个唯一的名字。这就是新的 make_unique_nickname 方法所做的(文件model.py):

 @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”,依次下去直至找到唯一的用户名。需要注意的是我们把这个方法作为一个静态方法,因为这种操作并不适用于任何特定的类的实例。

第二个存在重复昵称问题的地方就是编辑用户信息的视图函数。这个稍微有些难处理,因为这是用户自己选择的昵称。正确的做法就是不接受一个重复的昵称,让用户重新输入一个。我们将通过添加一个昵称表单字段定制化的验证来解决这个问题。如果用户输入一个不合法的昵称,字段的验证将会失败,用户将会返回到编辑用户信息页。为了添加验证,我们只需覆盖表单的 validate 方法(文件form.py):

class EditForm(Form):
nickname = StringField('nickname', validators=[DataRequired()])
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 =model.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_nicknamevalidate 方法使用它来决定昵称什么时候更改过。如果没有发生更改就接受它。如果已经发生更改的话,确保昵称在数据库是唯一的。

在视图中传入这个参数:

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

为了完成这个修改,我们必须在表单模板中使得字段错误信息会显示(文件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>

现在我们来测试下结果:

五、单元测试框架

随着应用程序的规模变得越大就越难保证代码的修改不会影响到现有的功能。

传统的方式–回归测试是一个很好的主意。你编写测试检验应用程序所有不同的功能。每一个测试集中在一个关注点上验证结果是不是期望的。定期执行测试确保应用程序按预期的工作。当测试覆盖很大的时候,通过运行测试你就有自信确保修改点和新增点不会影响应用程序。

我们使用 Python 的 unittest 模块将会构建一个简单的测试框架(文件 tests.py):

 #!flask/bin/python
import os
import unittest
from config import basedir
from microblog import app,db
class TestCase(unittest.TestCase):
def setUp(self):
app.config['TESTING'] = True
app.config['WTF_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_make_unique_nickname(self):
from model import User
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()

TestCase 类中含有我们的测试。setUp 和 tearDown 方法是特别的,它们分别在测试之前以及测试之后运行。(在 setUp 中做了一些配置,在 tearDown 中重置数据库内容。)

运行:python test.py

flask_单元测试的更多相关文章

  1. Intellij idea添加单元测试工具

    1.idea 版本是14.0.0 ,默认带有Junit,但是不能自动生成单元测试,需要下载JunitGererator2.0插件 2.Settings -Plugins,下载 JunitGenerat ...

  2. Python的单元测试(二)

    title: Python的单元测试(二) date: 2015-03-04 19:08:20 categories: Python tags: [Python,单元测试] --- 在Python的单 ...

  3. Python的单元测试(一)

    title: Python的单元测试(一) author: 青南 date: 2015-02-27 22:50:47 categories: Python tags: [Python,单元测试] -- ...

  4. javascript单元测试框架mochajs详解

    关于单元测试的想法 对于一些比较重要的项目,每次更新代码之后总是要自己测好久,担心一旦上线出了问题影响的服务太多,此时就希望能有一个比较规范的测试流程.在github上看到牛逼的javascript开 ...

  5. 使用NUnit为游戏项目编写高质量单元测试的思考

    0x00 单元测试Pro & Con 最近尝试在我参与的游戏项目中引入TDD(测试驱动开发)的开发模式,因此单元测试便变得十分必要.这篇博客就来聊一聊这段时间的感悟和想法.由于游戏开发和传统软 ...

  6. 我这么玩Web Api(二):数据验证,全局数据验证与单元测试

    目录 一.模型状态 - ModelState 二.数据注解 - Data Annotations 三.自定义数据注解 四.全局数据验证 五.单元测试   一.模型状态 - ModelState 我理解 ...

  7. ABAP单元测试最佳实践

    本文包含了我在开发项目中经历过的实用的ABAP单元测试指导方针.我把它们安排成为问答的风格,欢迎任何人添加更多的Q&A's,以完成这个列表. 在我的项目中,只使用传统的ABAP report. ...

  8. python_单元测试unittest

    Python自带一个单元测试框架是unittest模块,用它来做单元测试,它里面封装好了一些校验返回的结果方法和一些用例执行前的初始化操作. 步骤1:首先引入unittest模块--import un ...

  9. .Net中的AOP系列之《单元测试切面》

    返回<.Net中的AOP>系列学习总目录 本篇目录 使用NUnit编写测试 编写和运行NUnit测试 切面的测试策略 Castle DynamicProxy测试 测试一个拦截器 注入依赖 ...

随机推荐

  1. excel 导入 sqlserver 字符串被截取为255长度解决方案

    excel表格导入sqlserver数据表中 内容被截取为255长度的字符串. 注意:excel是通过前8行(表头的首行除外)的数据类型来判断导入数据的数据格式的,例如前8行出现整数型,那么默认就用整 ...

  2. 2016 China-Final-F题 ——(SA+二分)

    其实是一个很经典的字符串问题,但是我们比赛的时候没出. 先看一下UVA11107这题,题意是,找出最长的一个字符串,在至少一半的字符串中出现过.只要把所有的字符串用不同的分隔符分开,然后SA一下,最后 ...

  3. 转载:Solr的自动完成实现方式(第一部分:facet方式)

    转自:http://www.cnblogs.com/ibook360/archive/2011/11/30/2269059.html 大部分人已经见过自动完成(autocomplete)的功能了(见下 ...

  4. Odoo 中的 Controller

    来自  Odoo处理HTTP请求的接口用的Contoller类,封装于web模块中. --------------------------------------------------------- ...

  5. 什么是业务运维,企业如何实现互联网+业务与IT的融合

    业务运维并不是一个新概念,针对传统信息架构提出的业务服务管理就是把以业务为核心的IT系统与IT基础设施性能进行整合运维的解决方案.然而随着互联网+转型的不断推进,基础设施的智能化和广泛云化成为IT发展 ...

  6. postgres 批量更新内容

    在程序中遇到这样的需求, 数据库表格式如下 需要把批量更新status, 如name = fox 时, status = 1, name = boa 时,status = 2 .... 类似的 pos ...

  7. CSS 图片倾斜的制作

    <style> #zhong{ height:600px; width:1350px; position:relative; z-index:2} .znei{ height:60px; ...

  8. nginx + lua + redis 防黑IP

    lua脚本 local redis = require "resty.redis" local red = redis.new() red.connect(red, '127.0. ...

  9. docker--buildbot安装

    curl -L https://github.com/docker/compose/releases/download/1.8.0/docker-compose-`uname -s`-`uname - ...

  10. Multi-armed Bandit Problem与增强学习的联系

    选自<Reinforcement Learning: An Introduction>, version 2, 2016, Chapter2 https://webdocs.cs.ualb ...