overview
test is Software Development过程in important 环节, 它可以helping我们发现 and 修复bug, 确保application quality and reliability. Flaskapplicationtest主要including单元test, 集成test, APItestetc.class型. 本章节将介绍such as何usingPython testframework (such asunittest and pytest) 来testFlaskapplication, includingtestenvironmentconfiguration, test用例writing, test覆盖率analysisetc. in 容.
1. testBasics
1.1 testclass型
- 单元test - testapplicationin 单个function or method, verification它们 behavior is 否符合预期
- 集成test - testapplicationin many 个component之间 交互, verification它们能否协同工作
- APItest - testapplication API端点, verification它们 response is 否符合预期
- 端 to 端test - test整个application 流程, from user输入 to 最终输出
1.2 testframework
Pythonin has many 个流行 testframework:
- unittest - Python标准libraryin testframework, providing了完整 testfunctions
- pytest - 一个第三方testframework, providing了更简洁 语法 and 更 many functions
- nose2 - unittest scale, providing了更 many functions and 更 good 报告
in 本章节in, 我们将主要usingpytest来testFlaskapplication, 因 for 它providing了更简洁 语法 and 更丰富 functions.
2. testenvironmentconfiguration
2.1 installationtest依赖
pip install pytest pytest-flask coverage
2.2 projectstructure
一个典型 Flaskprojectteststructuresuch as under :
myapp/
├── app/ # applicationcode
│ ├── __init__.py
│ ├── models.py
│ ├── routes.py
│ └── templates/
├── tests/ # testcode
│ ├── __init__.py
│ ├── conftest.py # testconfigurationfile
│ ├── test_routes.py # routingtest
│ ├── test_models.py # modeltest
│ └── test_api.py # APItest
├── app.py # application入口
└── requirements.txt
2.3 testconfigurationfile
in testTable of Contentsin, 我们可以creation一个conftest.pyfile来configurationtestenvironment:
# tests/conftest.py
import pytest
from app import create_app, db
@pytest.fixture
def app():
"""creationtestapplicationinstance"""
# usingtestconfiguration
app = create_app({
'TESTING': True,
'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',
'SQLALCHEMY_TRACK_MODIFICATIONS': False
})
# creationdatalibrary表
with app.app_context():
db.create_all()
yield app
# 销毁datalibrary表
with app.app_context():
db.drop_all()
@pytest.fixture
def client(app):
"""creationtest客户端"""
return app.test_client()
@pytest.fixture
def runner(app):
"""creationtestCLIrun器"""
return app.test_cli_runner()
3. testFlaskapplication
3.1 testrouting
我们可以usingtest客户端来testapplication routing:
# tests/test_routes.py
def test_home_page(client):
"""test首页routing"""
response = client.get('/')
assert response.status_code == 200
assert b'欢迎来 to Flaskapplication' in response.data
def test_about_page(client):
"""test关于页面routing"""
response = client.get('/about')
assert response.status_code == 200
assert b'About Us' in response.data
3.2 test表单submitting
我们可以test表单submittingfunctions:
# tests/test_routes.py
def test_register_form(client):
"""testregister表单"""
response = client.post('/register', data={
'username': 'testuser',
'email': 'test@example.com',
'password': 'password'
}, follow_redirects=True)
assert response.status_code == 200
assert b'register成功' in response.data
3.3 testdatalibraryoperation
我们可以testdatalibrarymodel and operation:
# tests/test_models.py
from app.models import User
from app import db
def test_user_creation(app):
"""testusercreation"""
with app.app_context():
# creationuser
user = User(username='testuser', email='test@example.com')
db.session.add(user)
db.session.submitting()
# queryuser
user = User.query.filter_by(username='testuser').first()
assert user is not None
assert user.email == 'test@example.com'
# updateuser
user.username = 'updateduser'
db.session.submitting()
user = User.query.filter_by(email='test@example.com').first()
assert user.username == 'updateduser'
# deleteuser
db.session.delete(user)
db.session.submitting()
user = User.query.filter_by(email='test@example.com').first()
assert user is None
4. testAPI端点
4.1 testGETrequest
我们可以testAPI GETrequest:
# tests/test_api.py
def test_get_users(client):
"""test获取userlist"""
response = client.get('/api/users')
assert response.status_code == 200
data = response.get_json()
assert data['status'] == 'success'
assert isinstance(data['data'], list)
def test_get_single_user(client):
"""test获取单个user"""
# 首先creation一个user
client.post('/api/users', json={
'name': 'testuser',
'email': 'test@example.com'
})
# 然 after 获取该user
response = client.get('/api/users/1')
assert response.status_code == 200
data = response.get_json()
assert data['status'] == 'success'
assert data['data']['name'] == 'testuser'
4.2 testPOSTrequest
我们可以testAPI POSTrequest:
# tests/test_api.py
def test_create_user(client):
"""testcreationuser"""
user_data = {
'name': 'testuser',
'email': 'test@example.com',
'age': 25
}
response = client.post('/api/users', json=user_data)
assert response.status_code == 201
data = response.get_json()
assert data['status'] == 'success'
assert data['data']['name'] == 'testuser'
assert data['data']['email'] == 'test@example.com'
4.3 testPUTrequest
我们可以testAPI PUTrequest:
# tests/test_api.py
def test_update_user(client):
"""testupdateuser"""
# 首先creation一个user
client.post('/api/users', json={
'name': 'testuser',
'email': 'test@example.com'
})
# 然 after update该user
response = client.put('/api/users/1', json={
'name': 'updateduser',
'age': 30
})
assert response.status_code == 200
data = response.get_json()
assert data['status'] == 'success'
assert data['data']['name'] == 'updateduser'
assert data['data']['age'] == 30
4.4 testDELETErequest
我们可以testAPI DELETErequest:
# tests/test_api.py
def test_delete_user(client):
"""testdeleteuser"""
# 首先creation一个user
client.post('/api/users', json={
'name': 'testuser',
'email': 'test@example.com'
})
# 然 after delete该user
response = client.delete('/api/users/1')
assert response.status_code == 204
# verificationuser已delete
response = client.get('/api/users/1')
assert response.status_code == 404
5. testauthentication and authorization
5.1 testloginfunctions
我们可以testuserloginfunctions:
# tests/test_auth.py
def test_login(client):
"""testuserlogin"""
# 首先creation一个user
client.post('/register', data={
'username': 'testuser',
'email': 'test@example.com',
'password': 'password'
})
# 然 after testlogin
response = client.post('/login', data={
'email': 'test@example.com',
'password': 'password'
}, follow_redirects=True)
assert response.status_code == 200
assert b'login成功' in response.data
assert b'欢迎, testuser' in response.data
# testerror password
response = client.post('/login', data={
'email': 'test@example.com',
'password': 'wrongpassword'
}, follow_redirects=True)
assert response.status_code == 200
assert b'邮箱 or passworderror' in response.data
5.2 test受保护 routing
我们可以test受保护 routing:
# tests/test_auth.py
def test_protected_route(client):
"""test受保护 routing"""
# 未login时访问受保护routing, 应该重定向 to login页面
response = client.get('/dashboard')
assert response.status_code == 302 # 重定向 to login页面
# login after 访问受保护routing
client.post('/register', data={
'username': 'testuser',
'email': 'test@example.com',
'password': 'password'
})
client.post('/login', data={
'email': 'test@example.com',
'password': 'password'
})
response = client.get('/dashboard')
assert response.status_code == 200
assert b'仪表盘' in response.data
5.3 testJWTauthentication
我们可以testJWTauthentication API端点:
# tests/test_api_auth.py
def test_protected_api_route(client):
"""test受JWT保护 APIrouting"""
# 未authentication时访问受保护API, 应该返回401
response = client.get('/api/protected')
assert response.status_code == 401
# login获取token
login_response = client.post('/api/login', json={
'email': 'admin@example.com',
'password': 'password'
})
assert login_response.status_code == 200
token = login_response.get_json()['data']['access_token']
# usingtoken访问受保护API
response = client.get('/api/protected', headers={
'Authorization': f'Bearer {token}'
})
assert response.status_code == 200
data = response.get_json()
assert data['status'] == 'success'
assert 'user_id' in data['data']
6. test覆盖率
test覆盖率 is 衡量testquality important 指标, 它表示test用例覆盖了how manycode. 我们可以usingcoveragetool来analysistest覆盖率.
6.1 installationcoverage
pip install coverage
6.2 run覆盖率test
# runtest并收集覆盖率data
coverage run -m pytest
# 生成coverage report
coverage report
# 生成HTML格式 coverage report
coverage html
6.3 coverage report解读
coverage report通常package含以 under 指标:
- 语句覆盖率 - 被执行 语句占总语句数 比例
- branch覆盖率 - 被执行 branch占总branch数 比例
- function覆盖率 - 被执行 function占总function数 比例
- 行覆盖率 - 被执行 行占总行数 比例
6.4 usingpytest-cov
我们还可以usingpytest-cov插件来更方便地run覆盖率test:
pip install pytest-cov
# runtest并生成coverage report
pytest --cov=app tests/
# 生成HTML格式 coverage report
pytest --cov=app --cov-report=html tests/
7. advancedtesttechniques
7.1 testfactory pattern
我们可以usingfactory pattern来creationtestdata:
# tests/factories.py
from factory import Factory, Faker
from app.models import User, Post
class UserFactory(Factory):
class Meta:
model = User
username = Faker('user_name')
email = Faker('email')
password = Faker('password')
class PostFactory(Factory):
class Meta:
model = Post
title = Faker('sentence')
content = Faker('text')
author = factory.SubFactory(UserFactory)
# in testinusing工厂
def test_post_creation(app):
"""test文章creation"""
with app.app_context():
# using工厂creationuser
user = UserFactory()
db.session.add(user)
db.session.submitting()
# using工厂creation文章
post = PostFactory(author=user)
db.session.add(post)
db.session.submitting()
assert post in user.posts
7.2 testmock
我们可以usingunittest.mock or pytest-mock来mock out 部依赖:
# tests/test_external_api.py
from unittest.mock import patch
def test_external_api_call(client, mocker):
"""test out 部API调用"""
# mock out 部APIresponse
mock_response = mocker.Mock()
mock_response.json.return_value = {'data': 'test'}
mock_response.status_code = 200
# mockrequests.getfunction
mocker.patch('requests.get', return_value=mock_response)
# 调用using out 部API routing
response = client.get('/external-api')
assert response.status_code == 200
data = response.get_json()
assert data['external_data'] == 'test'
7.3 parameter化test
我们可以usingparameter化test来test不同 输入值:
# tests/test_parametrized.py
import pytest
@pytest.mark.parametrize('username, email, password, expected_status', [
('testuser', 'test@example.com', 'password', 201), # has 效输入
('', 'test@example.com', 'password', 400), # user名 for 空
('testuser', '', 'password', 400), # 邮箱 for 空
('testuser', 'test@example.com', '', 400), # password for 空
('testuser', 'invalid-email', 'password', 400), # 无效邮箱
])
def test_register_parametrized(client, username, email, password, expected_status):
"""parameter化testregisterfunctions"""
response = client.post('/api/register', json={
'username': username,
'email': email,
'password': password
})
assert response.status_code == expected_status
8. testtool
8.1 Postman
Postman is a 流行 APItesttool, 它允许我们:
- 发送各种HTTPrequest
- test不同 requestparameter and request体
- verificationresponsestatus码 and responsedata
- creationtestcollection and automationtest
- 生成APIdocumentation
8.2 Insomnia
Insomnia is 另一个流行 APItesttool, 它providing了class似Postman functions, 但界面更简洁, functions更专注.
8.3 Selenium
Selenium is a 用于automation浏览器test tool, 它允许我们for端 to 端test, mockuser in 浏览器in operation.
from selenium import webdriver
def test_home_page_selenium():
"""usingSeleniumtest首页"""
driver = webdriver.Chrome()
driver.get('http://localhost:5000')
assert '欢迎来 to Flaskapplication' in driver.page_source
driver.quit()
8.4 Locust
Locust is a 用于loadtest tool, 它允许我们mock big 量userconcurrent访问application, testapplication performance and stable 性.
from locust import HttpUser, task, between
class WebsiteUser(HttpUser):
wait_time = between(1, 5)
@task
def index_page(self):
self.client.get("/")
@task
def about_page(self):
self.client.get("/about")
@task
def register(self):
self.client.post("/register", data={
"username": "testuser",
"email": "test@example.com",
"password": "password"
})
9. testbest practices
- test应该 is 独立 - 每个test用例应该独立run, 不依赖于othertest用例 结果
- test应该 is 可重复 - 每次runtest都应该得 to 相同 结果
- test应该 is fast 速 - test应该 in short 时间 in completion, 以便频繁run
- test应该覆盖主要functions - test应该覆盖application 主要functions and edge缘circumstances
- test应该易于maintenance - testcode应该清晰, 简洁, 易于understanding and maintenance
- usingtest驱动Development (TDD) - 先writingtest, 然 after writingimplementation, 确保code符合test要求
- continuous integration - in codesubmitting时自动runtest, 确保codequality
- 定期runtest - 定期run所 has test, 确保application stable 性
10. 实战example
让我们through一个完整 example来演示such as何testFlaskapplication.
10.1 applicationcode
# app.py
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password = db.Column(db.String(60), nullable=False)
def __repr__(self):
return f"<User {self.username}>"
@app.route("/")
def home():
return render_template('home.html')
@app.route("/register", methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
email = request.form['email']
password = request.form['password']
# checkuser名 and 邮箱 is 否已存 in
if User.query.filter_by(username=username).first():
flash('user名已被using', 'danger')
return redirect(url_for('register'))
if User.query.filter_by(email=email).first():
flash('邮箱已被register', 'danger')
return redirect(url_for('register'))
# creation new user
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
user = User(username=username, email=email, password=hashed_password)
db.session.add(user)
db.session.submitting()
flash('register成功!', 'success')
return redirect(url_for('home'))
return render_template('register.html')
@app.route("/login", methods=['GET', 'POST'])
def login():
if request.method == 'POST':
email = request.form['email']
password = request.form['password']
user = User.query.filter_by(email=email).first()
if user and bcrypt.check_password_hash(user.password, password):
flash('login成功!', 'success')
return redirect(url_for('home'))
else:
flash('邮箱 or passworderror', 'danger')
return render_template('login.html')
if __name__ == '__main__':
app.run(debug=True)
10.2 testcode
# tests/conftest.py
import pytest
from app import app, db
@pytest.fixture
def test_app():
# configurationtestenvironment
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
with app.app_context():
db.create_all()
yield app
with app.app_context():
db.drop_all()
@pytest.fixture
def client(test_app):
return test_app.test_client()
# tests/test_routes.py
def test_home_page(client):
"""test首页"""
response = client.get('/')
assert response.status_code == 200
# tests/test_auth.py
def test_register(client):
"""testregisterfunctions"""
response = client.post('/register', data={
'username': 'testuser',
'email': 'test@example.com',
'password': 'password'
}, follow_redirects=True)
assert response.status_code == 200
assert b'register成功' in response.data
def test_login(client):
"""testloginfunctions"""
# 首先register一个user
client.post('/register', data={
'username': 'testuser',
'email': 'test@example.com',
'password': 'password'
})
# 然 after testlogin
response = client.post('/login', data={
'email': 'test@example.com',
'password': 'password'
}, follow_redirects=True)
assert response.status_code == 200
assert b'login成功' in response.data
# tests/test_models.py
def test_user_model(test_app):
"""testusermodel"""
with test_app.app_context():
user = User(username='testuser', email='test@example.com')
db.session.add(user)
db.session.submitting()
assert user.id is not None
assert user.username == 'testuser'
assert user.email == 'test@example.com'
10.3 runtest
# run所 has test
pytest
# runspecificfile test
pytest tests/test_auth.py
# runspecifictest用例
pytest tests/test_auth.py::test_register
# 生成coverage report
pytest --cov=app tests/
summarized
本章节介绍了Flaskapplicationtest method and techniques, including:
- testBasicsconcepts and testclass型
- testenvironmentconfiguration and projectstructure
- usingpytesttestFlaskapplication routing, 表单submitting, datalibraryoperationetc.functions
- testAPI端点, includingGET, POST, PUT, DELETErequest
- testauthentication and authorizationfunctions
- test覆盖率analysis
- advancedtesttechniques, such astestfactory pattern, testmock, parameter化test
- 常用 testtool, such asPostman, Insomnia, Selenium, Locust
- testbest practices
- 完整 testexample
test is 确保Flaskapplicationquality and reliability important 手段, throughwriting and runtest, 我们可以发现 and 修复bug, 确保application functions符合预期. in practicalDevelopmentin, 我们应该养成良 good test习惯, writing全面 test用例, 并定期runtest.