Flask Vue.js全栈开发|第8章:单元测试

  • 原创
  • Madman
  • /
  • 2018-11-14 16:52
  • /
  • 0
  • 131 次阅读

flask vuejs 全栈开发-min.png

Synopsis: 未经测试的小猫,肯定不是一只好猫。本文是填补之前没有进行任何单元测试的坑,使用 Python 自带的 unittest 包,当然你也可以使用 pytest 包。另外,Flask 内建了一个测试客户端 app.test_client(),它能复现程序运行在 Web 服务器中的环境,扮演成客户端从而发送请求。为了查看我们的测试代码覆盖率,需要安装 coverage 包,并创建一个 Flask CLI 命令 - flask test,以后修改业务逻辑了,请跑一遍测试用例

代码已上传到 https://github.com/wangy8961/flask-vuejs-madblog/tree/v0.8 ,欢迎star

1. 测试框架: unittest

1.1 第一个测试

使用 Python 自带的 unittest 包来测试我们的 Flask 应用程序,创建 back-end/tests 目录,并新建 back-end/tests/__init__.py

from config import Config


class TestConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite://'

然后创建我们第一个单元测试 back-end/tests/test_basic.py,以 test 开头的文件可以被 unittest 找到:

import unittest
from flask import current_app
from app import create_app, db
from tests import TestConfig


class BasicsTestCase(unittest.TestCase):
    def setUp(self):
        '''每个测试之前执行'''
        self.app = create_app(TestConfig)  # 创建Flask应用
        self.app_context = self.app.app_context()  # 激活(或推送)Flask应用上下文
        self.app_context.push()
        db.create_all()  # 通过SQLAlchemy来使用SQLite内存数据库,db.create_all()快速创建所有的数据库表

    def tearDown(self):
        '''每个测试之后执行'''
        db.session.remove()
        db.drop_all()  # 删除所有数据库表
        self.app_context.pop()  # 退出Flask应用上下文

    def test_app_exists(self):
        self.assertFalse(current_app is None)

    def test_app_is_testing(self):
        self.assertTrue(current_app.config['TESTING'])

注意: 我们的调试函数都是以 test 开头,这样 unittest 就会将这些函数自动识别为 测试函数,并运行它们

1.2 Flask CLI 命令

使用 flask --help 可以查看 flask 命令支持哪些子命令:

(venv) D:\python-code\flask-vuejs-madblog\back-end>flask --help
Usage: flask [OPTIONS] COMMAND [ARGS]...

  A general utility script for Flask applications.

  Provides commands from Flask, extensions, and the application. Loads the
  application defined in the FLASK_APP environment variable, or from a
  wsgi.py file. Setting the FLASK_ENV environment variable to 'development'
  will enable debug mode.

    > set FLASK_APP=hello.py
    > set FLASK_ENV=development
    > flask run

Options:
  --version  Show the flask version
  --help     Show this message and exit.

Commands:
  db      Perform database migrations.
  routes  Show the routes for the app.
  run     Runs a development server.
  shell   Runs a shell in the app context.

修改 back-end/madblog.py

@app.cli.command()
def test():
    '''Run the unit tests.'''
    import unittest
    tests = unittest.TestLoader().discover('tests')  # 找到 tests 目录
    unittest.TextTestRunner(verbosity=2).run(tests)

此时,再次运行 flask --help

(venv) D:\python-code\flask-vuejs-madblog\back-end>flask --help
Usage: flask [OPTIONS] COMMAND [ARGS]...

  A general utility script for Flask applications.

  Provides commands from Flask, extensions, and the application. Loads the
  application defined in the FLASK_APP environment variable, or from a
  wsgi.py file. Setting the FLASK_ENV environment variable to 'development'
  will enable debug mode.

    > set FLASK_APP=hello.py
    > set FLASK_ENV=development
    > flask run

Options:
  --version  Show the flask version
  --help     Show this message and exit.

Commands:
  db      Perform database migrations.
  routes  Show the routes for the app.
  run     Runs a development server.
  shell   Runs a shell in the app context.
  test    Run the unit tests.

发现多了一个 flask test 命令,试一下看看:

(venv) D:\python-code\flask-vuejs-madblog\back-end>flask test
test_app_exists (test_basic.BasicsTestCase) ... ok
test_app_is_testing (test_basic.BasicsTestCase) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.242s

OK

2个测试都通过了

1.3 测试用户数据模型

新增 back-end/tests/test_user_model.py

import unittest
from app import create_app, db
from app.models import User
from tests import TestConfig


class UserModelTestCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app(TestConfig)
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()

    def test_password_hashing(self):
        u = User(username='john')
        u.set_password('pass1234')
        self.assertTrue(u.check_password('pass1234'))
        self.assertFalse(u.check_password('123456'))

    def test_avatar(self):
        u = User(username='john', email='john@163.com')
        self.assertEqual(u.avatar(128), ('https://www.gravatar.com/avatar/'
                                         '5ad2197b80f2010461c700d80fd35e9d'
                                         '?d=identicon&s=128'))

再次执行测试:

(venv) D:\python-code\flask-vuejs-madblog\back-end>flask test
test_app_exists (test_basic.BasicsTestCase) ... ok
test_app_is_testing (test_basic.BasicsTestCase) ... ok
test_avatar (test_user_model.UserModelTestCase) ... ok
test_password_hashing (test_user_model.UserModelTestCase) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.242s

OK

1.4 Flask 测试客户端

Flask 内建了一个测试客户端 app.test_client(),它能复现程序运行在 Web 服务器中的环境,扮演成客户端从而发送请求

新增 back-end/tests/test_api.py

from base64 import b64encode
from datetime import datetime, timedelta
import json
import re
import unittest
from app import create_app, db
from app.models import User, Post
from tests import TestConfig


class APITestCase(unittest.TestCase):
    def setUp(self):
        '''每个测试之前执行'''
        self.app = create_app(TestConfig)  # 创建Flask应用
        self.app_context = self.app.app_context()  # 激活(或推送)Flask应用上下文
        self.app_context.push()
        db.create_all()  # 通过SQLAlchemy来使用SQLite内存数据库,db.create_all()快速创建所有的数据库表
        self.client = self.app.test_client()  # Flask内建的测试客户端,模拟浏览器行为

    def tearDown(self):
        '''每个测试之后执行'''
        db.session.remove()
        db.drop_all()  # 删除所有数据库表
        self.app_context.pop()  # 退出Flask应用上下文

    ###
    # 404等错误处理
    ###
    def test_404(self):
        # 测试请求不存在的API时
        response = self.client.get('/api/wrong/url')
        self.assertEqual(response.status_code, 404)
        json_response = json.loads(response.get_data(as_text=True))
        self.assertEqual(json_response['error'], 'Not Found')

    ###
    # 用户认证相关测试
    ###
    def get_basic_auth_headers(self, username, password):
        '''创建Basic Auth认证的headers'''
        return {
            'Authorization': 'Basic ' + b64encode(
                (username + ':' + password).encode('utf-8')).decode('utf-8'),
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        }

    def get_token_auth_headers(self, username, password):
        '''创建JSON Web Token认证的headers'''
        headers = self.get_basic_auth_headers(username, password)
        response = self.client.post('/api/tokens', headers=headers)
        self.assertEqual(response.status_code, 200)
        json_response = json.loads(response.get_data(as_text=True))
        self.assertIsNotNone(json_response.get('token'))
        token = json_response['token']
        return {
            'Authorization': 'Bearer ' + token,
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        }

    def test_get_token(self):
        # 测试用户登录,即获取JWT,需要输入正确的用户名和密码,通过Basic Auth之后发放JWT令牌
        # 首先创建一个测试用户
        u = User(username='john', email='john@163.com')
        u.set_password('123')
        db.session.add(u)
        db.session.commit()

        # 输入错误的用户密码
        headers = self.get_basic_auth_headers('john', '456')
        response = self.client.post('/api/tokens', headers=headers)
        self.assertEqual(response.status_code, 401)

        # 输入正确的用户密码
        headers = self.get_basic_auth_headers('john', '123')
        response = self.client.post('/api/tokens', headers=headers)
        self.assertEqual(response.status_code, 200)
        json_response = json.loads(response.get_data(as_text=True))
        self.assertIsNotNone(json_response.get('token'))
        self.assertTrue(re.match(r'(.+)\.(.+)\.(.+)', json_response.get('token')))

    def test_not_attach_jwt(self):
        # 测试请求头Authorization中没有附带JWT时,会返回401错误
        response = self.client.get('/api/users/')
        self.assertEqual(response.status_code, 401)

    def test_attach_jwt(self):
        # 测试请求头Authorization中有附带JWT时,允许访问那些需要认证的API
        # 首先创建一个测试用户
        u = User(username='john', email='john@163.com')
        u.set_password('123')
        db.session.add(u)
        db.session.commit()
        # 附带JWT到请求头中
        headers = self.get_token_auth_headers('john', '123')
        response = self.client.get('/api/users/', headers=headers)
        self.assertEqual(response.status_code, 200)

    def test_anonymous(self):
        # 有些API不需要认证,比如 /api/posts/
        response = self.client.get('/api/posts/')
        self.assertEqual(response.status_code, 200)

更详细的测试代码,请参考: https://github.com/wangy8961/flask-vuejs-madblog/tree/master/back-end/tests

2. 代码覆盖报告: coverage

安装 coverage 包:

(venv) D:\python-code\flask-vuejs-madblog\back-end>pip install coverage
(venv) D:\python-code\flask-vuejs-madblog\back-end>pip freeze > requirements.txt

我们可以增强刚增加的 flask test 命令,给它添加 --coverage 选项:

import click
import os
import sys
...

app = create_app(Config)

# 创建 coverage 实例
COV = None
if os.environ.get('FLASK_COVERAGE'):
    import coverage
    COV = coverage.coverage(branch=True, include='app/*')
    COV.start()

...

@app.cli.command()
@click.option('--coverage/--no-coverage', default=False, help='Run tests under code coverage.')
def test(coverage):
    '''Run the unit tests.'''
    # 如果执行 flask test --coverage,但是FLASK_COVERAGE环境变量不存在时,给它配置上
    if coverage and not os.environ.get('FLASK_COVERAGE'):
        import subprocess
        os.environ['FLASK_COVERAGE'] = '1'  # 需要字符串的值
        sys.exit(subprocess.call(sys.argv))

    import unittest
    tests = unittest.TestLoader().discover('tests')
    unittest.TextTestRunner(verbosity=2).run(tests)

    if COV:
        COV.stop()
        COV.save()
        print('Coverage Summary:')
        COV.report()
        basedir = os.path.abspath(os.path.dirname(__file__))
        covdir = os.path.join(os.path.join(basedir, 'tmp'), 'coverage')
        COV.html_report(directory=covdir)
        print('')
        print('HTML report be stored in: %s' % os.path.join(covdir, 'index.html'))
        COV.erase()

此时,执行 flask test --coverage 命令:

(venv) D:\python-code\flask-vuejs-madblog\back-end>flask test --coverage
test_app_exists (test_basic.BasicsTestCase) ... ok
test_app_is_testing (test_basic.BasicsTestCase) ... ok
test_avatar (test_user_model.UserModelTestCase) ... ok
test_password_hashing (test_user_model.UserModelTestCase) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.287s

OK
Coverage Summary:
Name              Stmts   Miss Branch BrPart  Cover
---------------------------------------------------
app\__init__.py      27     10      0      0    63%
app\models.py        92     88     20      0     4%
---------------------------------------------------
TOTAL               119     98     20      0    15%

HTML report be stored in: D:\python-code\flask-vuejs-madblog\back-end\tmp\coverage\index.html

可以打开 back-end\tmp\coverage\index.html 查看 HTML 版本的代码覆盖报告:

1 coverage

3. 提交代码

$ git add .
$ git commit -m "8. 单元测试"
$ git checkout master
$ git merge dev
$ git branch -d dev

将本地 master 分支代码上传到 Github 代码仓库中的 master 分支:

$ git push -u origin master

打上标签 tag并上传:

$ git tag v0.8
$ git push origin v0.8

代码已上传到 https://github.com/wangy8961/flask-vuejs-madblog/tree/v0.8 ,欢迎star

未经允许不得转载: LIFE & SHARE - 王颜公子 » Flask Vue.js全栈开发|第8章:单元测试

分享

作者

作者头像

Madman

如果博文内容有误或其它任何问题,欢迎留言评论,我会尽快回复; 或者通过QQ、微信等联系我

0 条评论

暂时还没有评论.

发表评论前请先登录