使用 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 教程。