学习日记-2019530

Flask测试客户端、测试Web服务、端到端测试(Selenium)、http状态码、centos7无GUI下使用selenium、Youtobe上Flask课程学习、爬虫部分、SQLAlchemy复习

学习日记-2019530

Flask测试客户端

视图函数只能在请求上下文和运行中的程序里运行

在测试客户端中运行的视图函数和正常情况下的没有太大区别,服务器收到请求,将其分配给适当的视图函数,视图函数生成响应,将其返回给测试客户端。执行视图函数后,生成的响应会传入测试,检查是否正确。

运行Python flasky.py test遇到报错:

1
KeyError: <flask.cli.ScriptInfo object at 0x7fa84a4566a0>

后来查到:https://stackoverflow.com/questions/54107915/got-keyerror-flask-cli-scriptinfo-object-at-0x0000000003c05b70-when-running

解决

1
(microvenv) [root@~ microblog]# export FLASK_APP=flasky.py

但是运行结果如下,并没有执行客户端测试,可能是命令不对

1
2
3
4
5
6
7
(microvenv) [root@iz2ze8rern8nx1sglo8ub1z microblog]# python flasky.py test
test_password_setter (test_user_model.UserModelTestCase) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.596s

OK

好吧,是我把文件名写错了导致unittest发现不了以test_*开头的测试用例

1
2
3
4
5
6
7
8
(microvenv) [root@iz2ze8rern8nx1sglo8ub1z microblog]# python flasky.py test
test_home_page (test_client.FlaskClientTestCase) ... ok
test_password_setter (test_user_model.UserModelTestCase) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.915s

OK
  • 测试用例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"""
步骤:
客户端向首页发起一个请求
测试客户端调用get()方法得到结果Flask的Response对象->调用视图函数得到的响应
响应主体使用response.get_data()获取,get_data()得到的响应主体是一个字节数组
传入参数as_text=True得到Unicode字符串
self.assertTrue('Stranger' in response.get_data(as_text=True))
"""
import re
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('/')
self.assertEqual(response.status_code,200)
self.assertTrue('Stranger' in response.get_data(as_text=True))

使用post()方法发送包含表单数据的POST请求,Flask-WTF 生成的表单中包含一个隐藏字段,其内容是 CSRF 令牌,需要和表 单中的数据一起提交。为了复现这个功能,测试必须请求包含表单的页面,然后解析响应 返回的 HTML 代码并提取令牌,这样才能把令牌和表单中的数据一起发送。为了避免在测 试中处理 CSRF 令牌这一烦琐操作,最好在测试配置中禁用 CSRF 保护功能

test_client.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"""
测试先向注册路由提交一个表单
/auth/register路由有两种响应方式:注册数据可用,返回一个重定向,用户转到登录页面。
注册不可用,返回的响应会再次渲染注册表单。
注册成功,测试会检查响应的状态码是否为302,表示重定向
"""
# 测试注册与登录
def test_register_and_login(self):
response = self.client.post('/auth/register',data={
'email': 'nicezheng@foxmail.com',
'username': 'zhengjiani',
'password': 'cat',
'password2':'cat'
})
self.assertEqual(response.status_code,302)

auth/views.py

1
2
3
4
5
6
7
8
9
10
11
@auth.route('/register',methods=['GET','POST'])
def register():
form=RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data,password=form.password.data)
db.session.add(user)
db.session.commit()
token = user.generate_confirmation_token()
send_email(user.email,'Confirm Your Account','auth/email/confirm',user=user,token=token)
flash("确认邮件已经发至您邮箱")
return redirect('auth.login')

禁用CSRF保护功能

1
2
3
4
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:*'
WTF_CSRF_ENABLED = False

测试新账号登录失败

1
2
3
4
5
6
7
8
9
FAIL: test_register_and_login (test_client.FlaskClientTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/var/www/microblog/microblog/tests/test_client.py", line 42, in test_register_and_login
self.assertTrue(re.search('Hello,\s+zhengjiani!',response.get_data(as_text=True)))
AssertionError: None is not true

----------------------------------------------------------------------
Ran 3 tests in 1.563s

说明响应主体有问题,尝试把响应主体打印出来发现问题是模板渲染时错误

unconfirmed.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{% extends "base1.html" %}#这里引的我原来的基础模板导致出错

{% block title %}花茶书窝 - Confirm your account{% endblock %}

{% block page_content %}
<div class="page-header">
<h1>
Hello, {{ current_user.username }}!
</h1>
<h3>You have not confirmed your account yet.</h3>
<p>
Before you can access this site you need to confirm your account.
Check your inbox, you should have received an email with a confirmation link.
</p>
<p>
Need another confirmation email?
<a href="{{ url_for('auth.resend_confirmation') }}">Click here</a>
</p>
</div>
{% endblock %}

这就说明工程大的时候测试是十分必要的,需要一个模块一个模块的测试

response.get_data(as_text=True)返回的是渲染后的html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"""
测试刚才注册的账号的登录程序
通过向/auth/login路由发起POST请求完成,指定了参数follow_redirects=True,即返回的不是302状态码,而是请求重定向的URL返回的响应
"""
# 测试确认token
user = User.query.filter_by(email='nicezheng@foxmail.com').first()
token = user.generate_confirmation_token()
response = self.client.get('/auth/confirm/{}'.format(token),follow_redirects=True)
user.confirm(token)
self.assertEqual(response.status_code,200)
# print(response.get_data)
self.assertTrue(
'You have confirmed your account' in response.get_data(
as_text=True))

测试Web服务

测试API不使用cookie,所以无需配置。get_api_headers()是一个辅助方法,返回所有请求都要发送的通用首部,包括认证密令和MIME类型相关的首部。

很神奇,我测试权限的时候返回403状态码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 确保web服务拒绝没有提供认证密令的请求,返回401错误码
def test_no_auth(self):
response = self.client.get('/api/v1/posts/',content_type='application/json')
self.assertEqual(response.status_code,401)

======================================================================
FAIL: test_no_auth (test_api.APITestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/var/www/microblog/microblog/tests/test_api.py", line 43, in test_no_auth
self.assertEqual(response.status_code,401)
AssertionError: 403 != 401

----------------------------------------------------------------------
Ran 5 tests in 1.965s

FAILED (failures=1)

于是我搜了一下403和401状态码区别【下转http状态码】

收到403响应表示服务器完成认证过程,但是客户端请求没有权限去访问要求的资源。那就说明这个请求的时候服务器已经完成认证过程,what?

然后我打印了一下响应主体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def test_token_auth(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()

# issue a request with a bad token
response = self.client.get(
'/api/v1/posts/',
headers=self.get_api_headers('bad-token', ''))
print(response.get_data(as_text=True))
#self.assertEqual(response.status_code, 401)
1
test_token_auth (test_api.APITestCase) ... {"error":"forbidden","message":"Invalid credentials"}

看我们写的api/authentication.py

1
2
3
@auth.error_handler
def auth_error():
return unauthorized('Invalid credentials')

stackoverflow中说同一用户过于频繁的刷新服务器将导致403

1
2
3
Make sure you are caching the access tokens and that you are making a reasonable number of refresh requests. Refreshing in a tight loop (or too frequently from multiple threads or servers in a cluster) for the same user will lead to 403.

The 401 error is most likely due to the fact that getting a new access token failed and you are using an expired one.

然后考虑一下是否是权限问题导致,测试时使用的是用户权限,如果改成管理员呢

###提出issue,我有点方

大佬的回复,看来是我api代码有误

403issue.PNG

明天再检查。。。

端到端测试(Selenium)

因为我的microblog是在阿里云下,所以,对于无界面浏览器采用phantomjs+selenium

【下转:centos7无GUI下使用selenium】

不折腾,等部署好以后在windows下测试

http状态码

【来源】https://blog.csdn.net/grandPang/article/details/47448395

301和302状态码都表示重定向,浏览器在拿到这个状态码后会自动跳转到一个新的URL地址

301表示旧地址A的资源已经被永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址;302表示旧地址A的资源还在(仍然可以访问),这个重定向只是临时地从旧地址A跳转到地址B,搜索引擎会抓取新的内容而保存旧的网址。

301-代表永久性转移(Permanently Moved)

302-代表暂时性转移(Temporarily Moved )

【百度百科】

401错误码

HTTP 错误 401.1 - 未经授权:访问由于凭据无效被拒绝。

您的Web服务器认为,客户端(例如您的浏览器或我们的 CheckUpDown 机器人)发送的 HTTP 数据流是正确的,但进入网址 (URL) 资源 , 需要用户身份验证 , 而相关信息 1 )尚未被提供, 或 2 )已提供但没有通过授权测试。这就是通常所知的“ HTTP 基本验证 ”。 需客户端提供的验证请求在 HTTP 协议中被定义为 WWW – 验证标头字段 (WWW-Authenticate header field) 。

403错误码

403错误,表示资源不可用。服务器理解客户的请求,但拒绝处理它,通常由于服务器上文件或目录的权限设置导致的WEB访问错误。

【来源】https://juejin.im/post/5ad04750518825558b3e57bd

401 “Unauthenticated”
403 Forbidden

401是没有带认证信息或者带了错误的认证信息, 这时客户端可以修改认证信息进行重试; 而403是客户端带了正确的认证信息, 但服务器认为这个认证信息对应的用户是没有对应资源的访问权限的, 因此, 在向管理员获取相关权限之前, 是没有重试的必要的.

centos7无GUI下使用selenium

【来源】https://blog.csdn.net/xds2ml/article/details/52982748

vi /etc/yum.repos.d/google.repo

1
2
3
4
5
6
[google]
name=Google-x86_64
baseurl=http://dl.google.com/linux/rpm/stable/x86_64
enabled=1
gpgcheck=0
gpgkey=https://dl-ssl.google.com/linux/linux_signing_key.pub

yum install google-chrome-stable

yum update

yum install Xvfb

yum install libXfont

yum install xorg-x11-fonts*

chromedriver下载

wget https://chromedriver.storage.googleapis.com/74.0.3729.6/chromedriver_linux64.zip

Youtobe上Flask课程学习

1
2
3
4
#linux下
export FLASK_APP=flaskblog.py
#windows下
set FLASK_APP=flaskblog.py

查看一下 Nginx 的在 SELinux 中的类型 http_port_t 绑定的端口

1
2
3
root@iz2ze8rern8nx1sglo8ub1z ~]# semanage port -l | grep http_port_t
http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000
pegasus_http_port_t tcp 5988

开启debug模式

1
2
if __name__ == '__main__':
app.run(debug=True)

CSS引用

1
<link type="text/css" href="{{ url_for('static', filename='main.css') }}">

爬虫部分

1.中文词频统计jieba

2.词云https://wordart.com/login?next=/edit/gvh0mvzkyzem

SQLAlchemy复习

1
2
3
4
from flask_sqlalchemy import SQLAlchemy
app.config['SECRET_KEY']
app.config['SQLALCHEMY_DATABASE_URI']
db = SQLAlchemy(app)

数据库中一对多关系

1
2
3
4
5
6
7
8
9
10
11
12
"""
User model主要引用实际的Post Class
而外键引用实际上是“表的名字.字段名”
"""
class User(db.Model):
...
posts = db.relationship('Post',backref='author',lazy=True)

# 外键
class Post(db.Model):
...
user_id = db.Column(db.Integer,db.ForeignKey('user.id'),nullable=False)

一个表只能有一个主键,可以有多个唯一性索引,主键可以被其他字段作外键引用,而索引不能作为外键引用。

外键在其他表是主键,是唯一值。在本表可以有多个,如果其他表没有某个主键值,在本表就不能插入这个外键值。

1
2
3
4
5
6
7
8
9
10
11
db.create_all()
db.session.add(user_1)
db.session.commit()
User.query.all()
User.query.first()
User.query.filter_by(username='Corey').all()
User.query.filter_by(username='Corey').first()
user
user=User.query.get(1)
user.posts
post_1 = Post(title='Blog 1',content='First Post Content!',user_id=user.id)
1
db.drop_all()