主题
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 -ubash
flet test android --device-id emulator-5554 -uTIP
每张截图都要在与录制其 golden 相同的设备/模拟器上渲染——不同的屏幕尺寸和密度会产生 不同的像素。
运行测试
在桌面端
在你的应用目录中,运行 flet test。不带任何参数时,它会以当前 桌面平台为目标:
bash
uv run flet testbash
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 idsbash
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_screenshotbash
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=1bash
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 pytestbash
pytest若要在 pytest 下运行时查看实时的构建/启动输出(以及应用的 debugPrint 日志),请启用 debug 级别的 CLI 日志:
bash
uv run pytest -s -o log_cli=true -o log_cli_level=DEBUGpytest 自身没有用于指定设备目标或 golden 的选项,因此 flet test 的选项会映射到 环境变量:
flet test 选项 | 环境变量 |
|---|---|
[platform] | FLET_TEST_PLATFORM(例如 ios、android) |
-d, --device-id | FLET_TEST_DEVICE |
-u, --update-goldens | FLET_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 运行时,而非一个 模拟的近似实现。