flask_单元测试

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

In[2]: import model;
In[3]: from microblog import db;
In[4]: u=model.User(nickname="wll",email="[email protected]");
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 = [‘[email protected]‘]

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), ‘[email protected]‘ + 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):

 1 #!flask/bin/python
 2 import os
 3 import unittest
 4 from config import basedir
 5 from microblog import app,db
 6 class TestCase(unittest.TestCase):
 7     def setUp(self):
 8         app.config[‘TESTING‘] = True
 9         app.config[‘WTF_CSRF_ENABLED‘] = False
10         app.config[‘SQLALCHEMY_DATABASE_URI‘] = ‘sqlite:///‘ + os.path.join(basedir, ‘test.db‘)
11         self.app = app.test_client()
12         db.create_all()
13     def tearDown(self):
14         db.session.remove()
15         db.drop_all()
16     def test_make_unique_nickname(self):
17         from model import User
18         u = User(nickname=‘john‘, email=‘[email protected]‘)
19         db.session.add(u)
20         db.session.commit()
21         nickname = User.make_unique_nickname(‘john‘)
22         assert nickname != ‘john‘
23         u = User(nickname=nickname, email=‘[email protected]‘)
24         db.session.add(u)
25         db.session.commit()
26         nickname2 = User.make_unique_nickname(‘john‘)
27         assert nickname2 != ‘john‘
28         assert nickname2 != nickname
29
30 if __name__ == ‘__main__‘:
31     unittest.main()

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

运行:python test.py

时间: 2024-11-06 04:43:31

flask_单元测试的相关文章

单元测试Junit

###<center> 单元测试Junit </center>###- - -1.**单元测试**:> ==单元测试==是软件之中对于最小的功能模块的的测试,其可以对最基本的软件构成单元来测试.> 需要注意的是:> >**测试用例是用来达到测试想要的预期结果,而不能测试出程序的逻辑错误**. 2.**JUnit**:>1.**Junit是基于断言机制的**.是用于编写可复用测试集的简单框架,是xUnit的一个子集.xUnit是一套基于测试驱动开发的测试

MVC与单元测试实践之健身网站(四)-动作管理

网站后台负责进行动作的管理,包括动作名称.介绍.训练要点.配图等内容,以便前台能够使用这些内容.在上一篇< Fit项目图片上传和云存储的调通>中已经准备好了这里涉及到的主要技术难点,现在就开始完成该模块了. 一 列表介绍 健身管理模块包括肌群.肌肉的显示以及动作的管理.这儿也算是开始涉及"业务内容"了,还好我之前有储备了一些关于健身的资料,现在是时候派上另一种用场了. a) 肌群和肌肉因为内容相对固定,所以为了减少业务逻辑以及单元测试的代码量,当然最主要是为了偷懒,就只提供

MVC与单元测试实践之健身网站(二)-管理员模块

开始动手做这个项目时,发现无法做到完全的先设计.再编码,于是决定分模块进行,从管理员模块开始设计.编码,而且接口就已经改了好几次了. 管理员模块涉及的功能有登录和后台对管理员的维护,其中也涉及前端的开发.UI模板使用Inspinia,感觉这套模板功能丰富.界面美观,而且基于HTML5和BootStrap,对这两方面的知识也可以多些了解. 在上一篇<如何在单元测试时隔离ORM>中,解决了对Service层进行测试怎样构建伪对象的问题,随后管理员模块的Service层和单元测试在齐头并进中完成了:

单元测试(一)-NUnit基础

单元测试作为提高代码和软件质量的有效途径,其重要性和益处自不必多说,虽然我没有实践过TDD之类,但坚信单元测试的积极作用.作为一种开发方法,单元测试早在上世纪70年代就已经在Smalltalk语言被运用了,这么多年来,单元测试一次又一次证明了自身的价值,在各种开发方式此起彼伏的浪潮中,经受住了时间的考验. 现在,俺也开始学习了,并在以后好好实践.这个系列的学习素材为Roy Osherove所著The Art of Unit Testing with examples in C#, 2nd Edi

OA项目CRUD和单元测试(一)

使用ModeFirst方法生成数据库,EntityFramework5.0. 一:Model层的模型:(根据模型生成数据库) 二:Dal层的UserInfo代码: namespace SunOA.EFDAL { public class UserInfoDal { //crud DataModelContainer db = new DataModelContainer(); public UserInfo GetUserInfoById(int id) { return db.UserInfo

词频统计-单元测试

我自己的单元测试没有弄出来,我用c编的,在visual studio中貌似实现不了单元测试,而李俞寰同学是用c#编写的词频统计,在vs2015中实现单元测试无比的方便,所以我请教了他并借鉴了一下. [TestMethod()] public void DictionarySortTest() { Dictionary<string,int>input=new Dictionary<string,int>() { {"you,1}, {"are",1},

使用Xunit来进行单元测试

不管你爱与不爱,单元测试对于一个软件的长治久安还是必不可少的一环.在Visual Studio 2012后,VS中的测试浏览器也能与第三方的集成了,用起来还是非常方便的.目前在.Net框架下的测试工具主要有Nunit.内置的MSTest以及Xunit这三个工具,本文就简单的介绍一下如何在VS中使用XUnit这个测试框架的后起之秀. 安装Xunit: Xunit的安装现在不需要插件支持了,直接使用NuGet安装如下两个库即可: PM> Install-Package xunit PM> Inst

作业八——单元测试练习(个人练习)

必做一: 针对附录1给出的三角形判断Java 代码,应用等价类划分法设计测试用例,用表格形式列出设计的测试用例: 测试用例如下:(红色字体为错误预言) 序号 测试输入:三条边 测试预言:[Oracle:Illegal(非三角形),Scalene(一般三角形), Isoceles(等腰三角形),Regular(等边三角形)] 1 (5,5,5) Regular 2 (-5,-5,-5) Regular 3 (1,4,5) Illegal 4 (2,3,5) Illegal 5 (3,4,5) Sc

作业8:单元测试练习(个人练习)

要求 [必做题1] 针对附录1给出的三角形判断Java 代码,应用等价类划分法设计测试用例,用表格形式列出设计的测试用例,写到博客中.(10分) [必做题2] 模仿附录2给出的三角形判断Junit测试代码,设计单元测试脚本,测试 [必做题1]设计得到的测试用例.注意测试脚本中测试用例出现顺序与[必做题1]表格所列顺序一致.运行所得的测试脚本,截运行结果图,写到博客中,同时将源代码push到你自己的github.(20分) [必做题3] 心得体会.写下本次练习你收获的知识点(PS:测试用例设计方法