Skip to content

Testing

PythonNative is built so that the bulk of your application logic can be tested without a device or simulator. The reconciler talks to native widgets exclusively through the NativeViewRegistry; swap that out for a mock and a render produces a tree of plain Python dicts that pytest can introspect.

What to test

  • Components: assert that a component renders the expected element type with the expected props for a given input.
  • Hooks: drive state transitions and verify outputs after each render.
  • Reducers: pure functions; test them as you would any other Python function.
  • Native modules: skip platform-only paths or mock them at the boundary.

What not to test (or to test sparingly): the platform handler implementations themselves. Those run only on the device and benefit much more from manual smoke tests.

A minimal mock registry

from pythonnative.native_views import NativeViewRegistry, set_registry


class _MockHandler:
    def create_view(self, props):
        return {"props": dict(props), "children": []}

    def update_view(self, view, prev, next):
        view["props"] = dict(next)

    def add_child(self, parent, child, index):
        parent["children"].insert(index, child)

    def remove_child(self, parent, child):
        parent["children"].remove(child)

    def insert_child(self, parent, child, index):
        parent["children"].insert(index, child)


def install_mock_registry():
    reg = NativeViewRegistry()
    for ty in (
        "Text", "Button", "Column", "Row", "ScrollView",
        "View", "TextInput", "Image", "Switch", "Spacer",
        "Pressable", "FlatList",
    ):
        reg.register(ty, _MockHandler())
    set_registry(reg)

Drop this into a conftest.py and call install_mock_registry() from a session-scoped fixture, or as a fixture parameterized on the component under test.

Rendering a component in a test

create_page boots an _AppHost which is the same shape used at runtime. For tests we want a more direct path: invoke the reconciler with a known root and read its output.

import pythonnative as pn
from pythonnative.reconciler import Reconciler


def render(element):
    """Mount `element` once with the mock registry and return the root."""
    rec = Reconciler()
    rec.mount(element)
    return rec.root_view  # the mock dict for the root element

(For a longer-running test (effects, navigation), use create_page so you get the full lifecycle plumbing.)

Asserting on rendered output

def test_counter_increments():
    @pn.component
    def Counter():
        count, set_count = pn.use_state(0)
        return pn.Column(
            pn.Text(f"Count: {count}", key="t"),
            pn.Button("+", on_click=lambda: set_count(count + 1), key="b"),
        )

    install_mock_registry()
    root = render(Counter())

    label, button = root["children"]
    assert label["props"]["text"] == "Count: 0"

    button["props"]["on_click"]()
    # The reconciler re-renders synchronously; read the latest text.
    assert root["children"][0]["props"]["text"] == "Count: 1"

Notes:

  • key="t" and key="b" aren't required for a two-child column, but using them in tests makes assertions more robust as the component evolves.
  • Behavioural props (like on_click) are passed through unchanged, so tests can call them directly.

Testing hooks in isolation

For complex hook compositions (a custom hook that wraps several built-ins), wrap the hook in a tiny throwaway component and assert on its rendered shape:

def test_use_toggle():
    def use_toggle(initial=False):
        on, set_on = pn.use_state(initial)
        return on, lambda: set_on(not on)

    @pn.component
    def Probe():
        on, toggle = use_toggle()
        return pn.Text("on" if on else "off", on_click=toggle, key="t")

    root = render(Probe())
    assert root["props"]["text"] == "off"
    root["props"]["on_click"]()
    assert root["props"]["text"] == "on"

Testing native modules

Native modules call into platform SDKs directly, so unit-testing them with the real implementation requires a device. For most app tests it's enough to inject a fake at the boundary:

class FakeFs:
    def __init__(self):
        self.store = {}
    def write_text(self, path, content):
        self.store[path] = content
    def read_text(self, path):
        return self.store[path]

Pass the fake into your component (via a context, a default argument, or a module-level injection) and assert on store.

Running the suite

PythonNative uses pytest plus the standard CI matrix (Ruff, Black, MyPy). Run them all locally before pushing:

ruff check src/pythonnative
ruff format --check
black --check src/pythonnative
mypy src/pythonnative
pytest

The same commands run in CI on every push and pull request.

Next steps