Integration testing
Flet lets you write integration tests for your app and run them with the
flet test command. Tests drive your app the same way a
user would — finding controls by key or text, tapping buttons, entering text and
asserting the resulting UI — while the app runs on the target device exactly
as it ships: a built monolithic app with embedded Python.
Tests are written with pytest, so everything you
already know about pytest (fixtures, parametrization, markers, -k filtering)
just works.
flet test builds and runs your app the same way flet build
does, so the Flutter SDK and build prerequisites must be
installed. The first run provisions a test host (and downloads the SDK if
needed), which is slow; subsequent runs are cached and fast.
Where tests live
Put your tests in a tests/ directory at the root of your app — a sibling of
src/, not inside it (src/ is what gets packaged into the on-device app; your
test code stays on the host and drives the app):
my_app/
├── pyproject.toml
├── src/
│ └── main.py # your app
└── tests/
└── test_main.py # your tests
A new app created with flet create already includes a
tests/ folder with a sample test and the required pytest configuration in
pyproject.toml:
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
Enabling tests in an existing app
If your app wasn't created with flet create, enable testing by editing its
pyproject.toml.
1. Add the test dependencies to your development dependencies. The
flet[test] extra brings in pytest, pytest-asyncio and the
screenshot-comparison libraries:
- uv
- pip
[dependency-groups]
dev = [
# ...your existing dev dependencies...
"flet[test]",
]
[project.optional-dependencies]
dev = [
# ...your existing dev dependencies...
"flet[test]",
]
2. Configure pytest. asyncio_mode = "auto" is required — it runs each
async test on the same event loop as the flet_app fixture:
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
3. Create a tests/ directory and add your first test (see
Writing a test below).
Writing a test
Test functions are async and receive the flet_app
fixture, which starts your app and exposes a [tester][flet.testing.Tester] to
drive it. Each test gets a fresh app instance, so tests are independent and
can run in any order.
Here is the counter sample (tests/test_main.py) that flet create generates:
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
The matching app gives the button a key so the test can find it reliably:
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)
Give controls you want to test a stable key and
find them with find_by_key(). It's more
robust than matching on text, which can change with localization or formatting.
The tester API
flet_app.tester finds controls and drives
interactions. Finder methods return a Finder;
action methods take a Finder; and pump methods let the UI advance. All
methods are awaitable.
Finding controls
| Method | Finds controls by |
|---|---|
find_by_key(key) | their key |
find_by_text(text) | exact text |
find_by_text_containing(pattern) | a regular-expression match on text |
find_by_icon(icon) | their icon (e.g. ft.Icons.ADD) |
find_by_tooltip(value) | tooltip text |
A Finder reports how many controls matched (via
count) and lets you pick one with
first, last or
at():
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
Interacting
| Method | Action |
|---|---|
tap(finder) | tap a control |
long_press(finder) | long-press a control |
enter_text(finder, text) | type text into a field |
mouse_hover(finder) | hover the mouse over a control |
Pumping
The UI doesn't update instantly after an interaction. Call
pump_and_settle() to let the app process
events and render the result before asserting:
await tester.tap(await tester.find_by_key("submit"))
await tester.pump_and_settle()
assert (await tester.find_by_text("Done")).count == 1
Use pump(duration=...) to advance by a fixed amount
when you don't want to wait for everything to settle.
Screenshot testing
On Android and iOS you can capture a full-screen screenshot of the running app and compare it against a committed golden (reference) image — useful for catching visual regressions. Full-screen capture is a device feature, so this is not available on desktop.
tester.take_screenshot(name) captures
the screen as PNG bytes, and
flet_app.assert_screenshot(name, bytes)
compares them against the golden image, failing the test if they differ beyond a
similarity threshold (≈99% by default):
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 images are platform-specific and stored next to your tests, under
tests/golden/<platform>/<test_file>/<name>.png — commit them to your repository.
To record the goldens the first time (or update them after an intentional UI
change), run with -u (--update-goldens). This writes the captured screenshots
as the new reference instead of comparing:
- uv
- pip
uv run flet test android --device-id emulator-5554 -u
flet test android --device-id emulator-5554 -u
Render each screenshot on the same device/emulator you record its golden on — different screen sizes and densities produce different pixels.
Running tests
On desktop
From your app directory, run flet test. With no
arguments it targets the current desktop platform:
- uv
- pip
uv run flet test
flet test
On a mobile emulator/simulator or device
First, make sure a device is running. Use
flet emulators to list available emulators and
start one, then flet devices to get the id of a
running device:
- uv
- pip
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
flet emulators # list available emulators
flet emulators start <id> # start an emulator
flet devices # list running devices and their ids
Then pass the platform as the first argument and the device id with
--device-id (-d):
- uv
- pip
uv run flet test android --device-id emulator-5554
uv run flet test ios --device-id <simulator-id>
flet test android --device-id emulator-5554
flet test ios --device-id <simulator-id>
Useful options
| Option | Description |
|---|---|
[platform] | macos, linux, windows, ios, android (defaults to the current desktop) |
-d, --device-id | Target device/emulator id (required for ios/android) |
-k <expr> | Only run tests matching a pytest keyword expression |
--tests-dir <dir> | Directory containing the tests (default: tests) |
-v | Verbose — stream the underlying Flutter build/launch output |
Running specific tests
Use -k to run only the tests matching a pytest keyword expression — handy while
iterating on a single test:
- uv
- pip
uv run flet test -k test_screenshot
flet test -k test_screenshot
-k accepts the full pytest expression syntax, e.g. -k screenshot,
-k "increment or screenshot", or -k "not slow".
When running with pytest directly you can also select a test by its node id:
uv run pytest tests/test_main.py::test_increment # a single test
uv run pytest tests/test_main.py # one file
Running with pytest directly
Because tests are plain pytest, you can also run them with pytest. The Flet
pytest plugin provisions the test host on demand, so this works without running
flet test first (it targets the current desktop platform):
- uv
- pip
uv run pytest
pytest
To see the live build/launch output (and the app's debugPrints) while running
under pytest, enable CLI logging at debug level:
uv run pytest -s -o log_cli=true -o log_cli_level=DEBUG
pytest has no options of its own for the device target or goldens, so the
flet test options map to environment variables:
flet test option | Environment variable |
|---|---|
[platform] | FLET_TEST_PLATFORM (e.g. ios, android) |
-d, --device-id | FLET_TEST_DEVICE |
-u, --update-goldens | FLET_TEST_GOLDEN=1 |
# run on an iOS simulator and (re)record golden screenshots
FLET_TEST_PLATFORM=ios FLET_TEST_DEVICE=<simulator-id> FLET_TEST_GOLDEN=1 uv run pytest
When unset, pytest targets the current desktop platform and compares (rather
than records) screenshots.
flet test provisions a Flutter test host from your app (the same pipeline as
flet build), embeds your Python code, and runs it on the device. The test code
runs on your computer and drives the on-device app over an independent channel —
so you're testing your app exactly as it ships, including the embedded Python
runtime, not a simulated approximation.