Flask教程:http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-vii-unit-testing

SOURCE

这次课首先添加了404.html和500.html错误页面;然后配置通过邮件和文件记录日志;用重复nickename为例讲了问题解决办法;最后写了个单元测试框架。

补充内容 最后添加了测试覆盖范围检查工具Flash应用性能分析

404和500错误页面

首先设置视图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
    
    

页面模板

  • app/templates/404.html
    
    {% extends "base.html" %}
    
    {% block content %}
      <h1>File Not Found</h1>
      <p><a href="{{ url_for('index') }}">Back</a></p>
    {% endblock %}
    
    
  • app/templates/500.html
    
    {% extends "base.html" %}
    {% block content %}
        <h1>An unexpected error has occurrred</h1>
        <p>The administrator has been notified. Sorry for the inconvenience!</p>
        <p><a href="{{ url_for('index') }}">Home</a></p>
    {% endblock %}
    
    

日志邮件和日志文件

添加打开用户页面的链接__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
        from logging.handlers import RotatingFileHandler
        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)
        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)
        mail_handler.setLevel(logging.ERROR)
        file_handler.setLevel(logging.INFO)
        app.logger.addHandler(mail_handler)
        app.logger.addHandler(file_handler)
        app.logger.info('microblog startup')
    
    

当在run.py中设置app.run(debug=False),出现错误不会输出给客户(浏览器)。

[NOTE] 使用python的smtp模块把发送邮件在控制台中输出:

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

解决nickname重复的问题

在app/views.py的after_login中添加nickname = User.make_unique_nickname(nickname)

    
    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)
        db.session.add(user)
        db.session.commit()
    
    

为用户类中添加make_unique_nickname(nickname)。 app/models.py

    
    class User(db.Model):
    # ...
    @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
    # ...
    
    

对于用户配置文件编辑页面中,如果用户自己输入的用户已经存在,那么给出重新输入提示。主要是覆写表单验证方法 app/forms.py

    
    from app.models import User
    
    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 = 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
    
    

记得更新编辑页面的视图设置app/views.py

    
    @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>
    
    

单元测试

    
    #!flask/bin/python
    import os
    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['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_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()
    
    

运行单元测试flask/Scripts/python tests.py,如果测试用例都通过,显示类似这样:

$ flask/bin/python test.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.057s

OK

测试范围覆盖

查看这里Test Coverage部分。

应用性能分析

查看这里Profiling for performance部分。

Python自带了性能分析工具cProfile Flask有一个扩展为其提供应用性能分析Werkzeug 使用下面的profile.py替换run.py,启动应用就可以获得应用分析报表了。

    
    #!flask/bin/python
    from werkzeug.contrib.profiler import ProfilerMiddleware
    from app import app

    app.config['PROFILE'] = True
    app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30])
    app.run(debug = True)