Flaskapplicationtest

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.

codeexample

以 under is a 完整 Flaskapplicationtestexample, usingpytest and Flask test客户端:

from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    
    def __repr__(self):
        return f"<User {self.name}>"

@app.route('/api/users', methods=['GET'])
def get_users():
    users = User.query.all()
    return jsonify([{
        'id': user.id,
        'name': user.name,
        'email': user.email
    } for user in users])

@app.route('/api/users', methods=['POST'])
def create_user():
    data = request.get_json()
    user = User(name=data['name'], email=data['email'])
    db.session.add(user)
    db.session.submitting()
    return jsonify({
        'id': user.id,
        'name': user.name,
        'email': user.email
    }), 201

@app.route('/api/users/', methods=['GET'])
def get_user(user_id):
    user = User.query.get_or_404(user_id)
    return jsonify({
        'id': user.id,
        'name': user.name,
        'email': user.email
    })

@app.route('/api/users/', methods=['DELETE'])
def delete_user(user_id):
    user = User.query.get_or_404(user_id)
    db.session.delete(user)
    db.session.submitting()
    return '', 204

# testcode
import pytest

@pytest.fixture
def client():
    app.config['TESTING'] = True
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
    
    with app.app_context():
        db.create_all()
    
    with app.test_client() as client:
        yield client
    
    with app.app_context():
        db.drop_all()

def test_get_users(client):
    """test获取userlist"""
    response = client.get('/api/users')
    assert response.status_code == 200
    assert response.get_json() == []

def test_create_user(client):
    """testcreationuser"""
    user_data = {'name': 'testuser', 'email': 'test@example.com'}
    response = client.post('/api/users', json=user_data)
    assert response.status_code == 201
    data = response.get_json()
    assert data['name'] == 'testuser'
    assert data['email'] == 'test@example.com'

def test_get_single_user(client):
    """test获取单个user"""
    # 首先creation一个user
    user_data = {'name': 'testuser', 'email': 'test@example.com'}
    client.post('/api/users', json=user_data)
    
    # 然 after 获取该user
    response = client.get('/api/users/1')
    assert response.status_code == 200
    data = response.get_json()
    assert data['name'] == 'testuser'

def test_delete_user(client):
    """testdeleteuser"""
    # 首先creation一个user
    user_data = {'name': 'testuser', 'email': 'test@example.com'}
    client.post('/api/users', json=user_data)
    
    # 然 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

if __name__ == '__main__':
    pytest.main(['-v'])

练习题

  1. creation一个Flaskapplication, implementation simple 博客functions, including文章 增删改查
  2. for 该applicationwriting单元test, testmodel and 业务逻辑
  3. for 该applicationwriting集成test, testrouting and 视graphfunction
  4. for 该applicationwritingAPItest, test所 has API端点
  5. usingcoveragetoolanalysistest覆盖率
  6. usingpytest-mockmock out 部依赖
  7. usingparameter化testtest不同 输入值