使用 Selenium 测试分页
最后修改日期:2025 年 3 月 22 日
本教程详细介绍了如何构建一个具有分页功能的 Flask 应用程序,并集成了 SQLite 数据库。它包括生成 100 行示例数据,并使用 Selenium 对分页功能进行自动化测试。该应用程序遵循 Flask 推荐的应用程序工厂模式,以实现模块化和可扩展性。
项目结构
下面将描述应用程序的结构。
flask_pagination/ ├── instance/ │ └── example.db (SQLite database, created at runtime) ├── flask_pagination/ │ ├── __init__.py (Application factory) │ ├── db.py (Database initialization and utilities) │ ├── routes.py (Route definitions) │ └── templates/ │ └── index.html (HTML template) ├── tests/ │ └── test_app.py (Selenium tests) ├── run.py (Entry point to run the app) └── requirements.txt
设置 Flask 应用
该应用程序使用工厂模式 (create_app) 来实现模块化,并将 SQLite 数据库存储在 instance/ 目录中以实现隔离。它实现了分页功能,每页显示 10 条记录,共 100 条记录,并包含用于设置数据库的 CLI 命令 (init-db 和 populate-db)。数据库使用 items 表进行初始化,并填充了 100 行,标签从“Item 1”到“Item 100”。Selenium 测试配置了一个测试应用和数据库,验证了跨页面的分页功能,并在测试后清理了临时文件。
入口点
此脚本作为启动 Flask 应用程序的入口点,利用应用程序工厂模式进行实例化。
from flask_pagination import create_app
if __name__ == '__main__':
app = create_app()
app.run(debug=True)
run.py 文件是启动 Flask 应用程序的主要可执行文件。它从 flask_pagination 包导入 create_app 函数,并调用它来实例化应用程序对象。app.run(debug=True) 命令以启用调试模式启动开发服务器,这有助于在开发过程中进行实时错误跟踪和自动重新加载。
应用程序工厂
此模块建立应用程序工厂,配置基本设置并集成蓝图和数据库实用程序。
from flask import Flask
import click
import os
def create_app(test_config=None):
# Create and configure the app
app = Flask(__name__, instance_relative_config=True)
# Ensure the instance folder exists
try:
os.makedirs(app.instance_path)
except OSError:
pass
# Load configuration
if test_config is None:
app.config.from_mapping(
DATABASE=os.path.join(app.instance_path, 'example.db'),
)
else:
app.config.from_mapping(test_config)
# Register database commands
from . import db
db.init_app(app)
# Register routes
from . import routes
app.register_blueprint(routes.bp)
return app
__init__.py 文件定义了 create_app 函数,这是 Flask 应用程序工厂模式的核心。它使用相对实例路径初始化 Flask 实例以存储配置文件,并使用 os.makedirs 确保实例目录存在。
配置是动态设置的:默认的 SQLite 数据库路径被分配,除非被测试配置覆盖。该函数通过 db.init_app 集成了数据库实用程序,并注册了路由蓝图,返回一个完全配置的应用程序实例。
数据库处理
此模块负责 SQLite 数据库连接、架构创建和数据填充,以及自定义 CLI 命令。
import sqlite3
import click
from flask import current_app, g
def get_db():
if 'db' not in g:
g.db = sqlite3.connect(
current_app.config['DATABASE'],
detect_types=sqlite3.PARSE_DECLTYPES
)
g.db.row_factory = sqlite3.Row
return g.db
def close_db(e=None):
db = g.pop('db', None)
if db is not None:
db.close()
def init_db():
db = get_db()
db.execute('DROP TABLE IF EXISTS items')
db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)')
db.commit()
def populate_db():
db = get_db()
for i in range(1, 101):
db.execute('INSERT OR IGNORE INTO items (id, name) VALUES (?, ?)', (i, f'Item {i}'))
db.commit()
def init_app(app):
app.teardown_appcontext(close_db)
app.cli.add_command(init_db_command)
app.cli.add_command(populate_db_command)
@click.command('init-db')
def init_db_command():
"""Initialize the database."""
init_db()
click.echo('Initialized the database.')
@click.command('populate-db')
def populate_db_command():
"""Populate the database with 100 sample items."""
populate_db()
click.echo('Database populated with 100 items.')
db.py 模块管理 Flask 应用程序的数据库操作。get_db 函数建立与 SQLite 数据库的连接,并将其存储在 Flask 的 g 对象中,以便在请求上下文内重用,返回的行是类字典对象。close_db 确保请求结束后关闭连接。
init_db 函数创建一个新的 items 表,删除任何现有的表,而 populate_db 插入 100 行(例如,“Item 1”到“Item 100”)。init_app 函数将这些功能与应用程序集成,并添加用于数据库管理的 CLI 命令(init-db 和 populate-db),从而增强了可用性。
路由
此模块使用 Flask 蓝图实现应用程序的路由和分页逻辑,以实现模块化。
from flask import Blueprint, render_template, request
from .db import get_db
import math
bp = Blueprint('main', __name__)
def get_items(page, per_page=10):
offset = (page - 1) * per_page
db = get_db()
items = db.execute('SELECT * FROM items LIMIT ? OFFSET ?', (per_page, offset)).fetchall()
total_items = db.execute('SELECT COUNT(*) FROM items').fetchone()[0]
return items, total_items
@bp.route('/')
def index():
page = request.args.get('page', 1, type=int)
per_page = 10
items, total_items = get_items(page, per_page)
total_pages = math.ceil(total_items / per_page)
return render_template('index.html',
items=items,
page=page,
total_pages=total_pages,
per_page=per_page)
routes.py 模块在一个名为 main 的蓝图中定义了应用程序的路由逻辑。get_items 函数从数据库检索一个项目页面,根据页码和每页的项目数(默认为 10)计算偏移量。
它使用 SQL 的 LIMIT 和 OFFSET 进行分页,并返回项目以及总数。index 路由处理根 URL,从查询参数中提取请求的页面,获取相应的项目,并使用包含当前页、总页数和每页项目数的页码数据渲染 index.html 模板。
模板
此模板渲染带有导航控件的分页项目表。
<!DOCTYPE html>
<html>
<head>
<title>Pagination Example</title>
<style>
.pagination {
margin: 20px 0;
}
.pagination a {
padding: 8px 16px;
text-decoration: none;
color: black;
}
.pagination a.active {
background-color: #4CAF50;
color: white;
}
</style>
</head>
<body>
<h1>Items List</h1>
<table>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
{% for item in items %}
<tr>
<td>{{ item['id'] }}</td>
<td>{{ item['name'] }}</td>
</tr>
{% endfor %}
</table>
<div class="pagination">
{% if page > 1 %}
<a href="?page={{ page - 1 }}">« Previous</a>
{% endif %}
{% for p in range(1, total_pages + 1) %}
<a href="?page={{ p }}" class="{% if p == page %}active{% endif %}">{{ p }}</a>
{% endfor %}
{% if page < total_pages %}
<a href="?page={{ page + 1 }}">Next »</a>
{% endif %}
</div>
</body>
</html>
index.html 模板显示一个分页项目表和导航链接。它包含内联 CSS 来设置分页控件的样式,并以绿色突出显示活动页面。表格使用 Jinja2 的 for 循环遍历 items 列表动态显示项目 ID 和名称。分页链接包括“上一页”和“下一页”按钮,这些按钮根据当前页数有条件显示,以及每个页面的编号链接,通过 range 循环生成。这种结构确保了用户界面直观地导航数据集。
Selenium 单元测试
此模块包含基于 Selenium 的单元测试,用于验证 Flask 应用程序的分页功能。
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import time
import os
from flask_pagination import create_app
class TestPagination(unittest.TestCase):
def setUp(self):
# Set up Flask app with test configuration including DATABASE
self.app = create_app({
'TESTING': True,
'DATABASE': os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'instance', 'test_example.db')
})
self.client = self.app.test_client()
# Initialize and populate database within app context
with self.app.app_context():
from flask_pagination.db import init_db, populate_db
init_db()
populate_db()
# Start Flask server in a separate thread
import threading
self.server_thread = threading.Thread(target=self.app.run, kwargs={'port': 5000})
self.server_thread.daemon = True
self.server_thread.start()
time.sleep(1) # Give server time to start
# Set up Selenium
self.driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
self.driver.get('https://:5000')
time.sleep(1) # Wait for page to load
def tearDown(self):
self.driver.quit()
# Clean up test database
with self.app.app_context():
db_path = self.app.config['DATABASE']
if os.path.exists(db_path):
os.remove(db_path)
def test_initial_page_load(self):
rows = self.driver.find_elements(By.XPATH, '//table//tr[td]')
self.assertEqual(len(rows), 10)
first_item = self.driver.find_element(By.XPATH, '//table//tr[td][1]/td[2]').text
last_item = self.driver.find_element(By.XPATH, '//table//tr[td][10]/td[2]').text
self.assertEqual(first_item, 'Item 1')
self.assertEqual(last_item, 'Item 10')
def test_pagination_next(self):
next_button = self.driver.find_element(By.LINK_TEXT, 'Next »')
next_button.click()
time.sleep(1)
rows = self.driver.find_elements(By.XPATH, '//table//tr[td]')
self.assertEqual(len(rows), 10)
first_item = self.driver.find_element(By.XPATH, '//table//tr[td][1]/td[2]').text
last_item = self.driver.find_element(By.XPATH, '//table//tr[td][10]/td[2]').text
self.assertEqual(first_item, 'Item 11')
self.assertEqual(last_item, 'Item 20')
def test_pagination_specific_page(self):
page_5 = self.driver.find_element(By.LINK_TEXT, '5')
page_5.click()
time.sleep(1)
rows = self.driver.find_elements(By.XPATH, '//table//tr[td]')
self.assertEqual(len(rows), 10)
first_item = self.driver.find_element(By.XPATH, '//table//tr[td][1]/td[2]').text
last_item = self.driver.find_element(By.XPATH, '//table//tr[td][10]/td[2]').text
self.assertEqual(first_item, 'Item 41')
self.assertEqual(last_item, 'Item 50')
def test_last_page(self):
page_10 = self.driver.find_element(By.LINK_TEXT, '10')
page_10.click()
time.sleep(1)
rows = self.driver.find_elements(By.XPATH, '//table//tr[td]')
self.assertEqual(len(rows), 10)
first_item = self.driver.find_element(By.XPATH, '//table//tr[td][1]/td[2]').text
last_item = self.driver.find_element(By.XPATH, '//table//tr[td][10]/td[2]').text
self.assertEqual(first_item, 'Item 91')
self.assertEqual(last_item, 'Item 100')
if __name__ == '__main__':
unittest.main()
test_app.py 模块使用 Selenium 自动化测试 Flask 应用程序的分页功能。setUp 方法配置一个特定于测试的 Flask 实例,其中包含一个临时数据库,初始化并填充它,然后在单独的线程中启动服务器。初始化 Selenium 的 Chrome 驱动程序以访问应用程序。tearDown 通过关闭浏览器和删除测试数据库来确保清理。
test_initial_page_load 验证第一页加载了 10 条记录,项目从“Item 1”到“Item 10”。test_pagination_next 确认点击“下一页”显示“Item 11”到“Item 20”。test_pagination_specific_page 检查第五页显示“Item 41”到“Item 50”。test_last_page 确保第十页显示“Item 91”到“Item 100”。
每个测试都使用 XPath 定位表格行,并断言预期的内容,以验证分页的准确性。
需求
在 requirements.txt 中指定所需的 Python 包。
flask sqlite3 selenium webdriver-manager click
使用以下命令安装依赖项。
pip install -r requirements.txt
运行应用程序
要运行应用程序,请设置 FLASK_APP 环境变量。对于 Windows(命令提示符),请使用 set FLASK_APP=flask_pagination;对于 Unix/Linux/macOS,请使用 export FLASK_APP=flask_pagination;对于 Windows PowerShell,请使用 $env:FLASK_APP = "flask_pagination"。
然后,使用 flask init-db 初始化数据库,这将输出“Initialized the database”。使用 flask populate-db 填充数据库,它将确认“Database populated with 100 items”。通过 python run.py 启动应用程序,并在 https://:5000 访问它以查看分页表。
使用 python -m tests.test_app 运行测试。测试会生成一个临时数据库,执行检查,然后进行清理。
在本篇文章中,我们创建了一个 Flask 应用程序,它使用分页功能从 100 条总记录中每页显示 10 条记录。我们还编写了使用 Selenium 库来测试此功能的单元测试。
作者
列出 所有 Python 教程。