Skip to main content

pytest-qfield

A pytest plugin for testing QField QML plugins. This plugin uses pytest-qgis and QGIS behind the hood since QField does not have a python api.

Features

Fixtures

The following fixtures are provided by pytest-qfield:

  • qfield_bot: A QFieldBot instance to interact with the QField QML environment.
  • qfield_iface: A stub implementation of the QField application interface (iface in QML).
  • qfield_new_project: Initializes a new QField project.
  • main_window_qml_path: Path to the QML file used for the QField main window. Can be overridden to use a custom QML main window.

Stubs and Overriding Fixtures

pytest-qfield provides several stub fixtures that are automatically injected into the QML engine's context. You can override these fixtures in your conftest.py to provide custom behavior or extended versions of the stub classes.

Available Stubs

The following stub fixtures correspond to objects available in the QField QML context:

Fixture Name QML Context Property Description
qfield_iface iface QField application interface.
qgs_project_stub qgisProject QgsProject instance.
qfield_platform_utilities_stub platformUtilities Platform-specific utilities.
qfield_theme_stub Theme Theme colors, fonts, and layout constants. Required for QField versions >v4.0.6 where Theme is a C++ singleton.
qfield_string_utils_stub StringUtils String utility functions.
qfield_layer_utils_stub LayerUtils Layer utility functions.
qfield_feature_utils_stub FeatureUtils Feature utility functions.
qfield_geometry_utils_stub GeometryUtils Geometry utility functions.
qfield_settings_stub settings QSettings store backed by a per-test ini file, QML-callable via value, valueBool, setValue, and remove. Plugins use it as a cross-plugin channel (e.g. a project form button writes a token the plugin polls for).
qfield_qml_extra_context_properties (various) Dictionary of extra context properties to inject.

Named-Item Stubs

Some QField QML code locates objects through iface.findItemByObjectName("...") rather than as context properties. pytest-qfield auto-registers default stubs on the iface for the following object names:

Fixture Name Registered objectName Description
qfield_positioning_stub positionSource QField Positioning item — active flag and projectedPosition (x, y).
qfield_geometry_highlighter_stub geometryHighlighter GeometryHighlighter exposing geometryWrapper, duration, visible, and update().
qfield_feature_list_form_stub featureForm FeatureListForm — drive an existing feature's attribute form via model.setFeatures(layer, filter), selection.focusedItem, state ("Hidden" / "FeatureList" / "FeatureForm" read-only view / "FeatureFormEdit" edit), and visible (true whenever a feature is shown). selection.focusedFeature / focusedLayer resolve the focused occurrence from the last setFeatures call. The QML id is featureListForm but plugins look it up by objectName "featureForm".
qfield_overlay_feature_form_drawer_stub overlayFeatureFormDrawer overlayFeatureFormDrawer — the drawer that covers the map after digitizing a feature. Exposes opened. Distinct from featureForm.

For the form stub, tests typically drive the canonical pattern from QML and assert on the captured calls:

// in your plugin's QML
const form = iface.findItemByObjectName("featureForm");
form.model.setFeatures(layer, "id = '" + uuid + "'");
form.selection.focusedItem = 0;
form.state = "FeatureFormEdit";
def test_form_was_opened(qfield_feature_list_form_stub):
    (recorded_layer, recorded_filter), = (
        qfield_feature_list_form_stub.model.set_features_calls
    )
    assert recorded_layer.name == "points"
    assert recorded_filter == "id = 'abc'"
    assert qfield_feature_list_form_stub.state == "FeatureFormEdit"

Override the relevant fixture to inject custom values (e.g. a fixed position):

# conftest.py
import pytest
from pytest_qfield.stub_interface.qfield_stubs import QFieldPositioningStub

@pytest.fixture
def qfield_positioning_stub() -> QFieldPositioningStub:
    return QFieldPositioningStub(x=389870.0, y=6678167.0, active=True)

See test/test_named_item_overrides.py for a complete working example. To assert behaviour when a named item is missing, clear the registration on the iface stub or override the fixture to return a sentinel.

Map Canvas Stub

iface.mapCanvas() returns a QFieldMapCanvasStub exposing the QField MapCanvas signals plugins listen to:

Member Description
clicked(QPointF, int) signal Emitted by QField when the user taps the map.
confirmedClicked(QPointF, int) signal Emitted on a long-press / confirmed tap.
mapSettings.screenToCoordinate(QPointF) -> QPointF Delegates to the wired QgsMapCanvas.mapSettings().mapToPixel() for real screen→CRS projection (identity fallback when no canvas is wired).

The stub is auto-attached to the iface as qml_map_canvas and is also exposed via the qfield_map_canvas_stub fixture for overrides. By default the fixture wires pytest-qgis's qgis_canvas so projection works for real — set an extent and size on qgis_canvas, then drive clicks at project-CRS coordinates with qfield_bot.click_map_at / long_press_map_at:

from qgis.core import QgsPointXY, QgsRectangle

def test_pick_end_point(qfield_bot, qgis_canvas):
    qgis_canvas.show()
    qgis_canvas.resize(200, 200)
    qgis_canvas.setExtent(QgsRectangle(0, 0, 1000, 1000))
    qgis_canvas.refresh()

    qfield_bot.click_map_at(QgsPointXY(500.0, 500.0))
    # ...assert your plugin reacted to the picked CRS coordinate

    # confirmedClicked (QField's long-press) and non-default click types:
    qfield_bot.long_press_map_at(QgsPointXY(500.0, 500.0))
    qfield_bot.click_map_at(QgsPointXY(0.0, 0.0), click_type=1)

Both helpers invert the live mapSettings.mapToPixel() transform before emitting, so a plugin handler that does mapSettings.screenToCoordinate(point) recovers the CRS coordinate you passed in.

If you need to emit a raw screen-space click (or you've opted out of canvas wiring), reach the signal directly:

from PyQt6.QtCore import QPointF

qfield_bot.iface.qml_map_canvas.clicked.emit(QPointF(0.0, 0.0), 0)

If you'd rather skip the canvas setup and pass project-CRS coordinates directly, override the fixture to drop canvas wiring:

@pytest.fixture
def qfield_map_canvas_stub() -> QFieldMapCanvasStub:
    return QFieldMapCanvasStub()  # no canvas → identity screenToCoordinate

For richer modelling (custom signals, an extent property, etc.), subclass QFieldMapCanvasStub / QFieldMapSettingsStub and return your subclass from the fixture.

How to Override

To override a stub, subclass it and redefine the fixture in your conftest.py or test module. For example, to make StringUtils.createUuid() return a deterministic value:

# conftest.py
import pytest
from PyQt6.QtCore import pyqtSlot
from pytest_qfield.stub_interface.qfield_stubs import QFieldStringUtilsStub

class DeterministicStringUtils(QFieldStringUtilsStub):
    @pyqtSlot(result=str)
    def createUuid(self) -> str:
        return "{00000000-0000-0000-0000-000000000000}"

@pytest.fixture
def qfield_string_utils_stub() -> QFieldStringUtilsStub:
    return DeterministicStringUtils()

Any QML code calling StringUtils.createUuid() will now receive the fixed value. See test/test_fixture_override.py for a complete working example.

You can also use qfield_qml_extra_context_properties to inject additional objects into the QML context:

@pytest.fixture
def qfield_qml_extra_context_properties():
    return {
        "myCustomObject": MyCustomObject()
    }

Other overridable fixtures include:

  • main_window_qml_path: Override to use a different QML file as the main window shell.
  • register_qfield_resources: Override to register your own compiled Qt resources (.qrc).
  • register_qfield_types: Override to register additional QML types.
  • register_qgis_types: Override to register additional QGIS-related QML types.

QFieldBot

The qfield_bot fixture provides several methods to help testing:

  • load_plugin(qml_file): Loads a QField plugin QML file.
  • open_project(qfield_project_file): Opens a QField project file.
  • show_window(): Shows the QField main window.
  • get_item(object_name): Finds a QML item by its objectName.
  • click_item(item): Simulates a mouse click at the center of a QML item. Raises if that point is outside the QML window, where Qt would silently drop the click.
  • click_map_at(crs_point, click_type=0): Emits clicked on the QML map canvas stub for a tap at a project-CRS coordinate (inverts mapToPixel so the plugin's screenToCoordinate recovers the input).
  • long_press_map_at(crs_point, click_type=0): Emits confirmedClicked (QField's long-press gesture) at a project-CRS coordinate.
  • open_feature_form(layer, feature_id, mode="view"): Drives the featureForm stub as QField does when a feature is opened — navigates the model to the feature, sets state to the read-only "FeatureForm" view (mode="view") or "FeatureFormEdit" (mode="edit"), and makes the form visible. Notify signals fire so bound plugin QML reacts.
  • open_overlay_form(layer, feature_id): Opens the overlayFeatureFormDrawer stub (opened = True) for a feature, as QField does after digitizing.
  • close_forms(): Hides featureForm (visible = False, state = "Hidden") and closes overlayFeatureFormDrawer (opened = False).
  • load_js_function(js_file, function_name, params): Loads a JavaScript function from a file for direct testing.

Window layout

The QML window is stacked over qgis_canvas with the same geometry, as QField draws its UI over the map. QML scene coordinates therefore equal canvas pixels, so a tap on a full-map overlay passes the right point to mapCanvas.mapSettings.screenToCoordinate(). The plugin root fills the window, and iface.mainWindow().contentItem is a window-sized item that overlays can use as parent, as in QField. The plugins toolbar is drawn over the top of the map.

Examples

Installation

You must have QGIS >= 4.0 installed to use this plugin.

Install with pip or uv to a python environment that is aware of system QGIS libraries. PyQt6 is not installed as a dependency, as it must be the one QGIS was built against. You can create one with qgis-venv-creator.

pip install pytest-qfield
# uv add --dev pytest-qfield

If your QGIS PyQt6 lacks QML bindings (e.g. Debian, Ubuntu), the import error shows the command to install a matching PyQt6 wheel.

Configure QField source path

The plugin loads QML from QField source code. Clone QField, check out the target QField tag and point the plugin to the clone either with an environment variable or a pytest.ini value. Both QField 4.3+ (src/gui/qml) and older (src/qml/imports) layouts are detected.

QFIELD_SOURCE_DIR=/absolute/path/to/QField pytest
# pyproject.toml
[tool.pytest.ini_options]
qfield_source_dir = /absolute/path/to/QField

The former QFIELD_IMPORTS_DIR / qfield_imports_dir names still work but are deprecated.

Development environment

This project uses uv to manage python packages. Make sure to have it installed first.

  • Clone QField source code somewhere and checkout the tag for requested QField version.
  • Copy .env.example to .env and fill the missing values
  • Create a venv that is aware of system QGIS libraries: uv venv --system-site-packages. Make sure to use same Python executable as QGIS.
# Activate the virtual environment
$ source .venv/bin/activate
# Install dependencies
$ uv sync
# Install pre-commit hooks
$ prek install
# Run tests
$ uv run pytest

Updating dependencies

uv lock --upgrade

Release process

Releases are made automatically upon push to main branch using Python Semantic Release.

Contributing

Contributions are very welcome.

Inspirations

License

Distributed under the terms of the GNU GPL v2.0 license, "pytest-qfield" is free and open source software.

Metadata

Release files for pytest-qfield 0.11.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pytest-qfield 0.11.0
File Size Uploaded
pytest_qfield-0.11.0.tar.gz 37.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pytest-qfield 0.11.0
File Interpreter ABI Platform
pytest_qfield-0.11.0-py3-none-any.whl Python 3 none any Details

Total release size: 76.1 kB

Release files / pytest_qfield-0.11.0.tar.gz

Download URL pytest_qfield-0.11.0.tar.gz
Size 37.4 kB
Tags Source
SHA-256 checksum
How to use checksums
db3a4cc19588f65f2f83c31712893d858c9685ae49710e72f9c0f927758e356d
BLAKE2b-256 checksum
How to use checksums
c4a273b3dcf414c4c52fcee5a804763beb7a73a927087d2e01e0a0164ae69dba
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / pytest_qfield-0.11.0-py3-none-any.whl

Download URL pytest_qfield-0.11.0-py3-none-any.whl
Size 38.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d890da745152298b7feaf268228e9de101443ad5af0d113b3726ee6f25a31e8b
BLAKE2b-256 checksum
How to use checksums
a05ddfa495c77be510dd724e9136eeb0b450104c89aa470a70403dea06b294a7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.11.0 This release

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page