ZetCode

使用 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-dbpopulate-db)。数据库使用 items 表进行初始化,并填充了 100 行,标签从“Item 1”到“Item 100”。Selenium 测试配置了一个测试应用和数据库,验证了跨页面的分页功能,并在测试后清理了临时文件。

入口点

此脚本作为启动 Flask 应用程序的入口点,利用应用程序工厂模式进行实例化。

run.py
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) 命令以启用调试模式启动开发服务器,这有助于在开发过程中进行实时错误跟踪和自动重新加载。

应用程序工厂

此模块建立应用程序工厂,配置基本设置并集成蓝图和数据库实用程序。

flask_pagination/__init__.py
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 命令。

flask_pagination/db.py
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-dbpopulate-db),从而增强了可用性。

路由

此模块使用 Flask 蓝图实现应用程序的路由和分页逻辑,以实现模块化。

flask_pagination/routes.py
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 的 LIMITOFFSET 进行分页,并返回项目以及总数。index 路由处理根 URL,从查询参数中提取请求的页面,获取相应的项目,并使用包含当前页、总页数和每页项目数的页码数据渲染 index.html 模板。

模板

此模板渲染带有导航控件的分页项目表。

flask_pagination/templates/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 应用程序的分页功能。

tests/test_app.py
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 包。

requirements.txt
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 库来测试此功能的单元测试。

作者

我叫 Jan Bodnar,是一位充满热情的程序员,拥有多年的丰富经验。自 2007 年以来,我已撰写了超过 1400 篇编程文章和 8 本电子书。此外,我在教授编程概念方面拥有八年以上的经验。

列出 所有 Python 教程