测试
编写测试单元的目的主要有两个,实现新功能时,单元测试能够确保新添加的代码按预期方式运行,这个进程也可手动完成,不过自动化测试明显能有效节省时间和精力 另外一重要目的是每次修改程序后,运行单元测试能保证现有代码的功能没有退化, 也就是说改动没有影响原有代码的正常运行 在最开始,单元测试就是Flasky开发的1部份,我们为数据库模型类中实现的程序功能编写了测试,模型类很容易在运行中的程序上下文以外进行测试,因此不用花费太多精力,为数据库模型中是瞎玩呢的全部功能编写测试,这最少能有效保证程序这部份在不断完善的进程中仍能按预期运行 获得代码覆盖报告编写测试组件很重要,但知道测试的好坏一样重要,代码覆盖工具用来统计单元测试检查了多少程序功能,并提供1个详细的报告,说明程序的哪些代码没有测试到,这个信息非常重要,由于它能指引你为最需要测试的部份编写出新测试 Python提供了1个优秀的代码覆盖工具coverage,可使用pip安装 这个工具本身是1个命令行脚本,可以在任何1个Python程序中检查代码覆盖,除此以外它还提供了更方便的脚本访问功能,使用编程方式启动覆盖检查引擎,为了能更好地把覆盖监测集成到启动脚本
import os
COV = None
if os.environ.get('FLASK_COVERAGE'):
import coverage
COV = coverage.coverage(branch=True,include='app/*')
COV.start()
#...
@manager.command
def test(coveage=False):
'''Run the unit tests.'''
if coverage and not os.environ.get('FLASK_COVERAGE'):
import sys
os.environ['FLASK_COVERAGE'] = '1'
os.execvp(sys.executable,[sys.executable] + 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(basedir,'tmp/coverage')
COV.html_report(directory=covdir)
print('HTML version: file://%s/index.html' % covdir)
#... 在Flask-Script中,自定义命令很简单,若想为 不过,把代码覆盖集成到 函数 履行完所有测试后,
# (env) PS C:UsersBangysAppDataLocalGitHubflasky> python manage.py test
test_app_exists (test_basics.BasicsTestCase) ... ok
test_app_is_testing (test_basics.BasicsTestCase) ... ok
test_home_page (test_client.FlaskClientTestCase) ... ok
test_register_and_login (test_client.FlaskClientTestCase) ... ok
test_anonymous_user (test_user_model.UserModelTestCase) ... ok
test_duplicate_email_change_token (test_user_model.UserModelTestCase) ... ok
test_expired_confirmation_token (test_user_model.UserModelTestCase) ... ok
test_follows (test_user_model.UserModelTestCase) ... ok
test_gravatar (test_user_model.UserModelTestCase) ... ok
test_invalid_confirmation_token (test_user_model.UserModelTestCase) ... ok
test_invalid_email_change_token (test_user_model.UserModelTestCase) ... ok
test_invalid_reset_token (test_user_model.UserModelTestCase) ... ok
test_no_password_getter (test_user_model.UserModelTestCase) ... ok
test_password_salts_are_random (test_user_model.UserModelTestCase) ... ok
test_password_setter (test_user_model.UserModelTestCase) ... ok
test_password_verification (test_user_model.UserModelTestCase) ... ok
test_ping (test_user_model.UserModelTestCase) ... ok
test_roles_and_permissions (test_user_model.UserModelTestCase) ... ok
test_timestamps (test_user_model.UserModelTestCase) ... ok
test_to_json (test_user_model.UserModelTestCase) ... ok
test_valid_confirmation_token (test_user_model.UserModelTestCase) ... ok
test_valid_email_change_token (test_user_model.UserModelTestCase) ... ok
test_valid_reset_token (test_user_model.UserModelTestCase) ... ok
----------------------------------------------------------------------
Ran 23 tests in 10.205s
OK
Coverage Summary:
Name Stmts Miss Branch BrPart Cover
-------------------------------------------------------------------------
app__init__.py 33 0 0 0 100%
appapi_1_0__init__.py 3 0 0 0 100%
appapi_1_0authentication.py 30 19 10 0 28%
appapi_1_0comments.py 40 30 8 0 21%
appapi_1_0decorators.py 11 3 2 0 62%
appapi_1_0errors.py 17 10 0 0 41%
appapi_1_0posts.py 35 23 6 0 29%
appapi_1_0users.py 30 24 8 0 16%
appauth__init__.py 3 0 0 0 100%
appauthforms.py 45 6 8 2 77%
appauthviews.py 109 56 40 6 42%
appdecorators.py 14 3 2 0 69%
appemail.py 15 0 0 0 100%
appexceptions.py 2 0 0 0 100%
appmain__init__.py 6 0 0 0 100%
appmainerrors.py 20 15 6 0 19%
appmainforms.py 39 7 4 0 74%
appmainviews.py 169 120 30 2 27%
appmodels.py 243 59 40 5 73%
-------------------------------------------------------------------------
TOTAL 864 375 164 15 51%
HTML version: file://C:UsersBangysAppDataLocalGitHubflaskytmp/coverage/index.html 上述报告显示,整体覆盖率为51%,情况其实不糟,但也不太好,现阶段,模型类是单元测试的关注焦点,它包括243个语句,测试覆盖了其中72%的语句,很明显,main和auth蓝本中的 有了这个报告,我们就可以很容易肯定向测试组件中添加哪些测试以提高覆盖率,但遗憾的是,并不是程序的所有组成部份都能像数据库模型那样易于测试,所以我们要学习如何去测试视图函数,表单和模板 注意,由于排版,实例报告中省略了“Missing”列的内容,这1列显示测试没有覆盖的源码行,是1个由行号范围组成的长列表 Flask测试客户端程序的某些代码严重依赖运行中的程序所创建的环境,例如不能直接调用视图函数中的代码进行测试,由于这个函数可能需要访问Flask上下文全局变量,如 Flask内建了1个测试客户端用于解决(部份解决)这1问题,测试客户端能复现程序运行在Web服务器中的环境,让测试扮演成客户端从而发送要求 在测试客户端中运行的视图函数和正常情况下的没有太大区分,服务器收到要求,将其分配给适当的视图函数,视图函数生成响应,将其返回给测试客户端,履行视图函数后,生成的响应会传入测试,检查是不是正确 测试Web程序下例是1个使用测试客户端编写的单元测试框架 #tests/test_client.py
import unittest
from app import create_app,db
from app.models import User,Role
class FlaskClientTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
Role.insert_roles()
self.client = self.app.test_client(use_cookies=True)
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_home_page(self):
response = self.client.get(url_for('main.index'))
self.assertTrue('Stranger' in response.get_data(as_text=True))
测试用例中的实例变量
测试客户端还能使用post()方法发送包括表单数据的POST要求,不过提交表单时会有1个小麻烦,Flask-WTF生成的表单中包括1个隐藏字段,其内容是CSRF令牌,需要和表单中的数据1起提交,为了复现这个功能,测试必须要求包括表单的页面,然后解析响应返回的HTML代码并提取令牌,这样才能把令牌和表单中的数据1起发送,为了不在测试中处理CSRF令牌这1繁琐操作,最好在测试配置中禁用CSRF保护功能,实现方法以下: # config.py
class TestingConfig(Config):
#...
WTF_CSRF_ENABLED = False 下面是1个更高级的单元测试,摹拟了新用户注册账户、登录、使用令牌确认账户和退出的进程 # test/text_client.py
class FlaskClientTestCase(unittest.TestCase):
def text_register_and_login(self):
# new account
response = self.clinet.post(url_for('auth.register'),data={
'email':'john@example.com','username':'john','password':'cat','passwprd2':'cat'
})
self.assertTrue(response.status_code == 302)
# use new account login
response = self.clinet.post(url_for('auth.login'),'password':'cat'
}m follow_redirects=True)
data = response.get_data(as_text=True)
self.assertTrue(re.search('Hello,s+john',data))
self.assertTrue('You have not confirmed your account yet' in data)
# send confirm token
user = User.query.filter_by(email = 'john@example.com').first()
token = user.generate_confirmation_token()
response = self.client.get(url_for('auth.comfirm',token=token),follow_redirects=True)
data = response.get_data(as_text=True)
self.assertTrue('You have comfirmed your account' in data)
#quit
response = self.client.get(url_for('auth.logout'),follow_redirects=True)
data = response.get_data(as_text=True)
self.assertTrue('You have been logged out' in data) 这个测试先向注册路由提交1个表单,
这个测试的第2部份使用刚才注册时使用的电子邮件和密码登录程序,这1工作通过向 成功登录后的响应应当是1个页面,显示1个包括用户名的欢迎消息,并提示用户需要进行账户确认才能取得权限,为此,两个断言语句被用于检查响应是不是为这个页面,值得注意的是,直接搜索字符串“Hello,john!”并没有用,由于这个字符串由动态部份和静态部份组成,而且两部份之间有额外的空白,为了不测试时空白引发的问题,我们使用更加灵活的正则表达式 下1步我们要确认账户,这里也有1个小障碍,在注册进程中,通过电子邮件将确认URL发给用户,而在测试中处理电子邮件不是1件简单的事情,上面这个测试使用的解决方法是疏忽了注册时生成的令牌,直接在 得到令牌后,测试的第3部份摹拟用户点击确认令牌URL,这1进程通过向确认URL发起GET要求并附上确认令牌来完成,这个要求的响应是重定向,转到首页,但这里再次指定了参数 这个测试的最后1步是向退前途由发送GET要求,为了证实确认退出,这段测试在响应中搜索1个Flash消息 测试Web服务Flask客户端还可用来测试REST Web服务,下例包括了两个测试: def get_api_headers(self,username,password):
return {
'Authorization':
'Basic ' + b64encode(
(username + ':' + password).encode('utf⑻')).decode('utf⑻'),'Accept':'application/json','Content-Type':'application/json'
}
def test_no_auth(self):
response = self.client.get(url_for('api.get_posts'),content_Type='application/json')
self.assertTrue(response.status_code == 401)
def test_posts(self):
# add a user
r = Role.query.filter_by(name="User").first()
self.assertIsNotNone(r)
u = User(email='john@example.com',password='cat',confirmed=True,role=r)
db.session.add(u)
db.session.commit()
#write a post
response = self.clinet.post(
url_for('api.new_post'),header=self.get_auth_header('john@example.com','cat'),data=json.dumps({'body': 'body of the *blog* post'}))
self.assertTrue(response.status_code == 201)
url = response.headers.get('Location')
self.assertIsNotNone(url)
#receive post
response = self.client.get(
url,headers=self.get_auth_header('john@example.com','cat'))
self.assertTrue(response_status_code == 200)
json_response = json.loads(response.data.decode('utf⑻'))
self.assertTrue(json_response['url'] == url)
self.assertTrue(json_response['body'] == 'body of the *blog* post')
self.assertTrue(json_response['body_html'] ==
'<p>body of the <em>blog</em> post</p>')
测试API时使用的
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |