第122天:Flask 单元测试

图片

若是一个软件项目没有通过测试,就像作的菜里没加盐同样。Flask 做为一个 Web 软件项目,如何作单元测试呢,今天咱们来了解下,基于 unittest 的 Flask 项目的单元测试。html

什么是单元测试

单元测试是软件测试的一种类型。顾名思义,单元测试的对象是程序中的最小的单元,能够是一个函数,一个类,也能够是它们的组合。python

相对于模块测试、集成测试以及系统测试等高级别的测试,单元测试通常由软件开发者而不是独立的测试工程师完成,且具备自动化测试的特质,所以单元测试也属于自动化测试。git

在实际开发中,有一些测试建议:github

  • 测试单元应该关注于尽量小的功能,要能证实它是正确的
  • 每一个测试单元必须是彻底独立的,必须能单独运行
  • 修改代码后,须要从新执行一次测试代码,以确保本次修改不会影响到其余部分
  • 提交代码前,须要执行一次完整测试,以确保不会将不完整或者错误的代码提交,影响其余开发者
  • 测试代码要和正常代码有明显的区分,测试代码文件应该是独立的

unittest 模块

Python 有不少单元测试框架,unittest、nose、pytest 等等,unittest 是 Python 内置的测试库,也是不少测试框架的基础,地位如同 Java 中的 JUnit,因此有时也被称做 PyUnit。数据库

unittest 支持 自动化测试、能够在多个测试中 共享设置测试环境和撤销测试环境代码能够将分散的测试集中起来,而且能够支持多种测试报告框架,所以 unittest 有四种重要概念:编程

  • test fixture 测试先后须要作些准备和清理工做,例如临时数据库链接、测试数据建立、测试用服务器建立,以及测试后的清理和销毁,test fixture 提供了 setUptearDown 接口来完成这些事情,而且能够被多个测试方法所共享
  • test case 测试用例,是最小的测试单元,检测一个特定输入的响应结果,unittest 提供 TestCase 基类,以便开发者建立具体的测试用例类
  • test suite 暂且翻译成测试套餐吧,是多个测试用例、测试套餐的组合,为了将一组相关的测试组织起来的工具
  • test run 测试执行器是按照必定规则执行测试用例,记录并返回测试结果的组件

小试牛刀

unittest 不须要安装,直接导入,例如一个测试字符串方法的测试代码:json

import unittest
class TestStringMethods(unittest.TestCase):
   def test_upper(self):        self.assertEqual('foo'.upper(), 'FOO')
   def test_isupper(self):        self.assertTrue('FOO'.isupper())        self.assertFalse('Foo'.isupper())
   def test_split(self):        s = 'hello world'        self.assertEqual(s.split(), ['hello', 'world'])        # check that s.split fails when the separator is not a string        with self.assertRaises(TypeError):            s.split(2)
if __name__ == '__main__':    unittest.main()
  • 导入 unittest 模块
  • 建立一个测试字符串方法的测试类,继承之 unittest 的 TestCase
  • 编写测试方法,注意测试方法必须以 test 做为开头,这样才能被测试加载器识别,同时也是良好的编程习惯
  • TestCase 提供了不少检验方法,例如 assertEqualassertTrue 等等,用于对指望结果进行检测
  • 最后,若是最为主代码被运行,调用 unittest.main 执行全部测试方法

运行代码:flask

python testBase.py

或者数组

python -m unittest testBase.py

结果以下:浏览器

...----------------------------------------------------------------------Ran 3 tests in 0.000s
OK

能够看到,执行了三个测试,没有发现异常状况,. 表示测试经过,数量表示执行了的测试方法个数

测试执行器

unittest.main 只给出了概要测试结果,若是须要更详细的报告,能够用测试执行器来运行测试代码

将 unittest.main() 换成:

suite = unittest.TestLoader().loadTestsFromTestCase(TestStringMethods)unittest.TextTestRunner(verbosity=2).run(suite)
  • 利用测试加载器(TestLoader)建立了一个测试套餐(TestSuite)
  • 用测试执行器(TestRunner)执行测试代码
  • TestTestRunner 是将结果做为文本格式输出
  • 参数 verbosity=2 表示显示详细的测试报告

或者干脆为 unittest.main 提供参数 verbosity :unittest.main(verbosity=2)

运行结果以下:

test_isupper (__main__.TestStringMethods) ... oktest_split (__main__.TestStringMethods) ... oktest_upper (__main__.TestStringMethods) ... ok
----------------------------------------------------------------------Ran 3 tests in 0.000s
OK

Flask 单元测试

Flask 做为一个 Web 项目,大多数代码须要在 Web 服务器环境下运行

  • 因此须要为每一个单元测试模拟一个 Web 环境
  • 另外有些部分须要使用到数据库,因此还须要为这些测试准备一个数据库环境
  • 最后有些业务处理代码,好比加工数据,数据运算等,能够进行独立测试,不须要 Web 环境

建立了一个简单项目,经过工厂方法建立 Flask 应用,有数据库的读写,下面逐步说明下测试脚本,测试代码文件 testApp.py 与项目代码在同一目录下

初始化环境

import unittest
from app import create_appfrom model import db
class TestAPP(unittest.TestCase):    def setUp(self):        self.app = create_app(config_name='testing')        self.client = self.app.test_client()        with self.app.app_context():            db.create_all()
   def tearDown(self):        with self.app.app_context():            db.drop_all()
  • 引入 unittest 模块
  • 从 Flask 应用代码文件(app.py)中引入工厂方法 create_app
  • 从模型代码文件(model.py)中引入数据库实例 db
  • 建立测试类 TestAPP,继承自 unittest.TestCase
  • 定义 setUp 方法,用工厂方法初始化 Flask 应用
  • Flask 提供了测试应用的建立方法 test_client,返回测试应用实例
  • 在应用实体环境下,初始化数据库
  • 定义 tearDown 方法,在测试结束后销毁数据库中的结构和数据

简单测试

编写两个测试方法,分别对 Flask 应用的配置状况和首页进行测试:

def test_config(self):        self.assertEqual(self.app.config['TESTING'], True)        self.assertIsNotNone(self.app.config['SQLALCHEMY_DATABASE_URI'])
def test_index(self):    ret = self.client.get('/')    self.assertEqual(b'Hello world!', ret.data)
  • 定义测试方法 test_config 用来测试 Flask app 的配置是否正常
  • 由于测试方法时实体方法,因此从实体引用(self)中的 app 属性中,查看配置属性,注意测试应用 test_client 不能之间获取 Flask app 的配置
  • 检测 TESTING 的值是否为 True,另外检查数据库链接是否存在
  • 定义方法首页的方法 test_index,经过测试应用的 get 方法访问网站根目录
  • 检测访问后的结果,在示例中,首页返回了字符串,确认下是否正确

此时运行测试代码能够获得以下

test_config (__main__.TestAPP) ... oktest_index (__main__.TestAPP) ... ok
----------------------------------------------------------------------Ran 2 tests in 0.066s

测试表单提交

在 Web 项目中,有不少须要交互的功能,例如表单提交,数据存储和查询,在 unittest 测试框架中,借助 Flask 的测试应用 test_client 能够轻松应对

示例项目中,有模拟用户注册和登陆的功能,注册和登陆都须要提交数据,而且只有在注册后,才能进行登陆,因此将注册和登陆编写成单独的功能:

def login(self, username):    params = {'username': username}    return self.client.post('/login', data=params, follow_redirects=True)
def register(self, username):    params = {'username': username}    return self.client.post('/register', data=params, follow_redirects=True)
  • 定义登陆方法 login,接受一个用户名的参数(这里忽略了密码等登陆凭证)
  • 利用测试应用 test_client 的 post 方法,访问登陆地址,将提交的数据用词典数据结构经过 data 参数提交
  • 定义注册方法 register,接受一个用户名的参数(一样忽略了密码等其余信息)
  • 注册方法和登陆相似,除了注册提交地址
  • 注意到 post 的参数 follow_redirects,值为 True 的做用是支持浏览器跳转,即收到跳转状态码时会自动跳转,直到不是跳转状态码时才会返回
  • 登陆和注册方法能够处理更多的业务逻辑,最后将请求结果返回

有了注册和登陆的协助,测试方法就更明晰:

def test_register(self):    ret = self.register('bar')    self.assertEqual(json.loads(ret.data)['success'], True)
def test_login(self):    self.register('foo')    ret = self.login('foo')    return self.assertEqual(json.loads(ret.data)['username'], 'foo')
def test_noRegisterLogin(self):    ret = self.login('foo')    return self.assertEqual(json.loads(ret.data)['success'], False)
def test_login_get(self):    ret = self.client.get('/login', follow_redirects=True)    self.assertIn(b'Method Not Allowed', ret.data)
  • 定义了 4 个测试方法,分别时单独的注册,注册后登陆,未注册时的登陆,和用 get 方法请求登陆接口
  • 每种方法都调用了 login 或者 register 方法,因此代码逻辑会更简洁
  • 注册和登陆接口,返回的时 JSON 格式数据,须要用 json.loads 将其转化为 词典
  • assertIn 相似与 indexOf 方法,用来检测给定的字符串是否在结果中

运行上述的是测试,能够获得以下结果:

test_login (__main__.TestAPP) ... oktest_login_get (__main__.TestAPP) ... oktest_noRegisterLogin (__main__.TestAPP) ... oktest_register (__main__.TestAPP) ... ok
----------------------------------------------------------------------Ran 4 tests in 0.196s
OK

您可能已经发现,测试执行的结果和测试方法定义的顺序不一致

缘由是测试加载器是按照测试名称字母顺序加载测试方法的,若是须要按照必定的顺序执行,须要用 TestSuite 设定执行顺序,如:

if __name__ == '__main__':    suite = unittest.TestSuite()    tests = [TestAPP('test_register'), TestAPP('test_login'), TestAPP('test_noRegisterLogin'), TestAPP('test_login_get')]    suite.addTests(tests)    runner = unittest.TextTestRunner(verbosity=2)    runner.run(suite)
  • 建立 TestSuite 实例
  • 将须要组织的测试方法放在数组中,用 TestSuiteaddTests 方法添加到 TestSuite 实例中
  • TestRuuner 运行 TestSuite 实例

这样就会以设定的顺序执行测试方法了

代码覆盖率

测试中有个重要的概念就是代码覆盖率,若是存在没有被被覆盖的代码,就有可能编写的测试代码不够全面

coverage Python 的一个测试工具,不只能够运行测试代码,还能够报告出代码覆盖率

安装

使用前,须要安装:

pip install coverage

执行测试

安装成功后,就能够在命令行中使用了,首先进入到测试代码的所在目录,

请注意  Python  包引用的查找位置,从不一样的目录运行,可能会影响到目录下模块的引用,例如在同一目录下,引用模块,若是在上一级目录中运行代码,可能出现找不到模块的错误,此时只须要相对于运行目录,调整下代码中模块引用方式就行了,具体可参见Python  Unit Testing – Structuring Your Project

执行以下命令:

coverage run testApp.py

结果以下:

test_config (__main__.TestAPP) ... oktest_index (__main__.TestAPP) ... oktest_login (__main__.TestAPP) ... oktest_login_get (__main__.TestAPP) ... oktest_noRegisterLogin (__main__.TestAPP) ... oktest_register (__main__.TestAPP) ... ok
----------------------------------------------------------------------Ran 6 tests in 0.226s

结果和之间运行测试代码相似,也就是说用 coverage run 命令能够代替 python 命令执行测试代码,例如

python -m unittest discover

将变为

coverage run -m unittest discover

覆盖率

coverage 更大的用处在于查看代码覆盖率,命令是 coverage report,例如:

coverage report testApp.py

结果以下:

Name         Stmts   Miss  Cover--------------------------------testApp.py      41      0   100%
  • Name 指的是代码文件名
  • Stmts 是执行的代码行数
  • Miss 表示没有被执行的行数
  • Cover 表示覆盖率,公式是(Stmts-Miss)/Stmts,即被执行代码所占比例,用百分比表示

若是要看到哪些行被忽略了,加上参数 -m 便可:

coverage report -m testApp.py

结果中会多一列 Missing,内容为执行的行号

代码覆盖率报告,是基于 coverage run 的运行结果的,因此没有测试的运行就没法获得覆盖率报告的

总体覆盖率报告

coverage run 在执行测试时,会记录全部被调用代码文件的执行状况,包括 Python 库中的代码,若是只想记录指定目录下的代码执行状况,须要用 --source 选项指定须要记录的目录,例如只记录当前目录下的执行状况:

coverage run --source . testApp.py

而后查看执行报告,例如:

Name         Stmts   Miss  Cover--------------------------------app.py          10      0   100%config.py       17      1    94%model.py        17      4    76%route.py        19      1    95%testApp.py      41      0   100%--------------------------------TOTAL          104      6    94%

若是执行时没有加上 --source 参数,也能够经过通配符文件名,指定要查看的代码文件:

coverage report *.py

结果同上

html 测试报告

若是项目中代码文件众多,在命令行中用文本方式显示测试报告就不太方便了,coverage html 能够将测试报告生成 html 文件,功能强大,显示效果更好:

coverage html -d testreport

参数 -d 用来指定测试报告存放的目录,若是不存在会建立

图片

文件名是个链接,点击能够看到文件内容,而且将执行和未执行的代码标注的很清楚:

图片

总结

今天介绍了 Flask 的单元测试,主要介绍了 Python 自带单元测试模块 unittest 的基本用法,以及 Flask 项目中单元测试的特色和方法,还介绍了 coverage 测试工具,以及代码覆盖率报告的用法。

最后须要强调的是:不管什么软件项目,单元测试是颇有必要的,单元测试不只能够确保项目的高质量交付,并且还为维护和查找问题节省了时间。

参考

https://medium.com/@neeti.jain/how-to-do-unit-testing-in-flask-and-find-code-coverage-fa5201399bc4https://www.patricksoftwareblog.com/python-unit-testing-structuring-your-project/https://coverage.readthedocs.io/en/coverage-5.0.3/index.htmlhttps://testerhome.com/topics/11655

示例代码:https://github.com/JustDoPython/python-100-day/tree/master/day-122


系列文章


第121天:机器学习之决策树
从 0 学习 Python 0 - 120 大合集总结

相关文章
相关标签/搜索