Skip to content

Flet 让你可以为应用编写集成测试,并通过 flet test 命令来运行。测试会以用户操作应用的方式来驱动它——按 key 或文本查找控件、点击按钮、输入文本,并断言最终的界面——而应用是在目标设备上 运行的,与正式发布时完全一致:一个内嵌了 Python 的、构建好的单体应用。

测试是用 pytest 编写的,所以你已有的 pytest 知识 (fixture、参数化、标记、-k 过滤)全部可以直接使用。

前置条件

flet test 构建并运行应用的方式与 flet build 相同,因此必须先 安装 Flutter SDK 及构建前置依赖。首次运行会准备一个测试托管环境 (并在需要时下载 SDK),耗时较长;之后的运行会走缓存,速度很快。

测试代码放在哪里 ​

把测试放在应用根目录下的 tests/ 目录里——它与 src/ 同级,而不是放在 src/ 内部 (src/ 里的内容会被打包进设备端应用;你的测试代码留在主机上,用来驱动应用):

my_app/
├── pyproject.toml
├── src/
│   └── main.py          # 你的应用
└── tests/
    └── test_main.py     # 你的测试

用 flet create 创建的新应用已经自带一个 tests/ 文件夹,内含 示例测试以及 pyproject.toml 中所需的 pytest 配置:

toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

在已有应用中启用测试 ​

如果你的应用不是用 flet create 创建的,可以通过编辑其 pyproject.toml 来启用测试。

1. 把测试依赖添加到开发依赖中。flet[test] 这个 extra 会一并引入 pytest、 pytest-asyncio 以及截图对比相关的库:

toml
[dependency-groups]
dev = [
    # ...你已有的开发依赖...
    "flet[test]",
]
toml
[project.optional-dependencies]
dev = [
    # ...你已有的开发依赖...
    "flet[test]",
]

2. 配置 pytest。 asyncio_mode = "auto" 是必需的——它会让每个异步测试与 flet_app fixture 运行在同一个事件循环上:

toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

3. 创建 tests/ 目录并添加你的第一个测试(见下方的 编写测试)。

编写测试 ​

测试函数是 async 的,并接收 [flet_app][flet.testing.FletTestApp] fixture,它负责 启动你的应用,并暴露出一个 [tester][flet.testing.Tester] 来驱动它。每个测试都会获得一个 全新的应用实例,因此测试之间彼此独立,可以以任意顺序运行。

下面是 flet create 生成的计数器示例(tests/test_main.py):

python
import flet.testing as ftt


async def test_increment(flet_app: ftt.FletTestApp):
    tester = flet_app.tester

    await tester.pump_and_settle()

    # Initial state
    assert (await tester.find_by_text("0")).count == 1

    # Tap the increment button (found by its key) and let the UI update
    await tester.tap(await tester.find_by_key("increment"))
    await tester.pump_and_settle()

    # New state
    assert (await tester.find_by_text("1")).count == 1

与之对应的应用给按钮设置了 key,这样测试就能稳定地找到它:

python
import flet as ft


def main(page: ft.Page):
    counter = ft.Text("0", size=50, data=0)

    def increment_click(e):
        counter.data += 1
        counter.value = str(counter.data)

    page.floating_action_button = ft.FloatingActionButton(
        icon=ft.Icons.ADD, key="increment", on_click=increment_click
    )
    page.add(
        ft.SafeArea(
            expand=True,
            content=ft.Container(content=counter, alignment=ft.Alignment.CENTER),
        )
    )


ft.run(main)

TIP

给你想要测试的控件设置一个稳定的 key,然后用 [find_by_key()][flet.testing.Tester.find_by_key] 去查找。这比按文本匹配更可靠,因为 文本可能会随本地化或格式化方式而改变。

tester API ​

[flet_app.tester][flet.testing.FletTestApp.tester] 用来查找控件并驱动交互。Finder 类方法会返回一个 [Finder][flet.testing.Finder];action 类方法接收一个 Finder; 而 pump 类方法则让界面得以推进。所有方法都是可 await 的。

查找控件 ​

方法按什么查找控件
[find_by_key(key)][flet.testing.Tester.find_by_key]控件的 key
[find_by_text(text)][flet.testing.Tester.find_by_text]精确文本
[find_by_text_containing(pattern)][flet.testing.Tester.find_by_text_containing]文本的正则表达式匹配
[find_by_icon(icon)][flet.testing.Tester.find_by_icon]控件的图标(如 ft.Icons.ADD)
[find_by_tooltip(value)][flet.testing.Tester.find_by_tooltip]工具提示文本

[Finder][flet.testing.Finder] 会报告匹配到了多少个控件(通过 [count][flet.testing.Finder.count]),并允许你用 [first][flet.testing.Finder.first]、[last][flet.testing.Finder.last] 或 [at()][flet.testing.Finder.at] 选出其中一个:

python
finder = await tester.find_by_text("Item")
assert finder.count == 3          # number of matches
await tester.tap(finder.first)    # first match
await tester.tap(finder.last)     # last match
await tester.tap(finder.at(1))    # match at index 1

交互 ​

方法动作
[tap(finder)][flet.testing.Tester.tap]点击一个控件
[long_press(finder)][flet.testing.Tester.long_press]长按一个控件
[enter_text(finder, text)][flet.testing.Tester.enter_text]在输入框中输入文本
[mouse_hover(finder)][flet.testing.Tester.mouse_hover]将鼠标悬停在一个控件上

推进界面 ​

界面不会在交互之后立即更新。在断言之前,请调用 [pump_and_settle()][flet.testing.Tester.pump_and_settle],让应用处理完事件并渲染出 结果:

python
await tester.tap(await tester.find_by_key("submit"))
await tester.pump_and_settle()
assert (await tester.find_by_text("Done")).count == 1

当你不想等待所有内容都稳定下来时,可以使用 [pump(duration=...)][flet.testing.Tester.pump] 按固定时长推进界面。

截图测试 ​

在 Android 和 iOS 上,你可以对运行中的应用截取全屏截图,并与已提交的 golden (参考)图片进行比对——这对捕获视觉回归很有用。全屏截图是一项设备功能,因此该能力在桌面端 不可用。

[tester.take_screenshot(name)][flet.testing.Tester.take_screenshot] 会把屏幕捕获为 PNG 字节,而 [flet_app.assert_screenshot(name, bytes)][flet.testing.FletTestApp.assert_screenshot] 会把它与 golden 图片进行比对,当差异超过相似度阈值(默认约 99%)时使测试失败:

python
async def test_home_screen(flet_app: ftt.FletTestApp):
    tester = flet_app.tester
    await tester.pump_and_settle()

    flet_app.assert_screenshot("home", await tester.take_screenshot("home"))

Golden 图片是与平台相关的,存放于测试代码旁边,路径为 tests/golden/<platform>/<test_file>/<name>.png——请将其提交到你的仓库中。

要首次录制 golden(或是在有意修改 UI 之后更新它们),请使用 -u(--update-goldens) 运行。这会将捕获到的截图作为新的参考写入,而不是进行比对:

bash
uv run flet test android --device-id emulator-5554 -u
bash
flet test android --device-id emulator-5554 -u

TIP

每张截图都要在与录制其 golden 相同的设备/模拟器上渲染——不同的屏幕尺寸和密度会产生 不同的像素。

运行测试 ​

在桌面端 ​

在你的应用目录中,运行 flet test。不带任何参数时,它会以当前 桌面平台为目标:

bash
uv run flet test
bash
flet test

在移动模拟器/仿真器或设备上 ​

首先,确保有一个设备正在运行。使用 flet emulators 列出可用的 模拟器并启动其中一个,然后使用 flet devices 获取正在运行的设备 的 id:

bash
uv run flet emulators              # list available emulators
uv run flet emulators start <id>   # start an emulator
uv run flet devices                # list running devices and their ids
bash
flet emulators              # list available emulators
flet emulators start <id>   # start an emulator
flet devices                # list running devices and their ids

然后,把平台作为第一个参数传入,并用 --device-id(-d)指定设备 id:

bash
uv run flet test android --device-id emulator-5554
uv run flet test ios --device-id <simulator-id>
bash
flet test android --device-id emulator-5554
flet test ios --device-id <simulator-id>

常用选项 ​

选项说明
[platform]macos、linux、windows、ios、android(默认为当前桌面)
-d, --device-id目标设备/模拟器 id(运行 ios/android 时必需)
-k <expr>只运行匹配 pytest 关键字表达式的测试
--tests-dir <dir>包含测试的目录(默认:tests)
-v详细模式——实时输出底层的 Flutter 构建/启动日志
-- <args>将 -- 之后的参数传递给 pytest,例如 -- -x --maxfail=1

运行特定测试 ​

使用 -k 只运行匹配 pytest 关键字表达式的测试——在单独迭代某个测试时很方便:

bash
uv run flet test -k test_screenshot
bash
flet test -k test_screenshot

-k 接受完整的 pytest 表达式语法,例如 -k screenshot、-k "increment or screenshot", 或 -k "not slow"。

其它 pytest 选项都放在 -- 分隔符之后,该分隔符会把其后的所有内容原样传给 pytest:

bash
uv run flet test -k screenshot -- -x --maxfail=1
bash
flet test -k screenshot -- -x --maxfail=1

当直接用 pytest 运行时,你还可以按节点 id 选择测试:

bash
uv run pytest tests/test_main.py::test_increment   # 单个测试
uv run pytest tests/test_main.py                   # 单个文件

直接用 pytest 运行 ​

由于测试就是普通的 pytest,你也可以直接用 pytest 来运行。Flet 的 pytest 插件会按需准备 测试托管环境,因此无需先运行 flet test(该方式以当前桌面平台为目标):

bash
uv run pytest
bash
pytest

若要在 pytest 下运行时查看实时的构建/启动输出(以及应用的 debugPrint 日志),请启用 debug 级别的 CLI 日志:

bash
uv run pytest -s -o log_cli=true -o log_cli_level=DEBUG

pytest 自身没有用于指定设备目标或 golden 的选项,因此 flet test 的选项会映射到 环境变量:

flet test 选项环境变量
[platform]FLET_TEST_PLATFORM(例如 ios、android)
-d, --device-idFLET_TEST_DEVICE
-u, --update-goldensFLET_TEST_GOLDEN=1
bash
# 在 iOS 模拟器上运行,并(重新)录制 golden 截图
FLET_TEST_PLATFORM=ios FLET_TEST_DEVICE=<simulator-id> FLET_TEST_GOLDEN=1 uv run pytest

未设置时,pytest 会以当前桌面平台为目标,并进行比对(而非录制)截图。

工作原理

flet test 会从你的应用准备出一个 Flutter 测试托管环境(与 flet build 相同的流水线), 内嵌你的 Python 代码,并在设备上运行它。测试代码运行在你的电脑上,通过一个独立通道来驱动 设备端应用——因此你测试的是与正式发布完全一致的应用,包括内嵌的 Python 运行时,而非一个 模拟的近似实现。


关注我,学习大模型、大数据、Python等热门技术。