← Back to DevBytes

Testing NiceGUI Applications: Unit Tests to Integration

Testing NiceGUI Applications: From Unit Tests to Integration

NiceGUI has quickly become one of the most popular Python frameworks for building web-based user interfaces. Its declarative, Pythonic API makes it trivial to spin up dashboards, admin panels, and internal tools. But as applications grow, the question every developer eventually faces is: how do I test this? This tutorial walks you through a complete testing strategy for NiceGUI apps, starting from isolated unit tests and ending with full integration tests that drive a real browser.

Why Testing NiceGUI Matters

NiceGUI applications often mix UI logic, business logic, and asynchronous server behavior. Without tests, small changes can silently break event handlers, page layouts, or data bindings. A solid test suite gives you the confidence to refactor, the ability to catch regressions in CI, and a form of living documentation for how your UI is supposed to behave.

There are three broad layers of testing you should consider:

Setting Up Your Test Environment

Before writing tests, install the necessary dependencies. We will use pytest as the test runner, pytest-asyncio for async support, and playwright for browser-based integration tests.

pip install pytest pytest-asyncio nicegui playwright
playwright install chromium

Create a pytest.ini file at the root of your project to configure async mode and test discovery:

[pytest]
asyncio_mode = auto
testpaths = tests

A Sample NiceGUI Application

To make the examples concrete, let's build a small counter application. Save it as app.py:

from nicegui import ui, app

def compute_next_value(current: int, step: int) -> int:
    """Pure business logic — easy to unit test."""
    return current + step

def build_page():
    state = {"count": 0}

    ui.label("Counter App").classes("text-2xl font-bold")
    display = ui.label("0").classes("text-xl")

    def increment():
        state["count"] = compute_next_value(state["count"], 1)
        display.set_text(str(state["count"]))

    def reset():
        state["count"] = 0
        display.set_text("0")

    ui.button("Increment", on_click=increment)
    ui.button("Reset", on_click=reset)

@ui.page("/")
def index():
    build_page()

if __name__ in {"__main__", "__mp_main__"}:
    ui.run(host="0.0.0.0", port=8080)

Notice how the business logic (compute_next_value) is separated from the UI code. This separation is the foundation of testable NiceGUI applications.

Unit Testing Business Logic

Unit tests target pure functions that do not depend on NiceGUI. These are the fastest and most reliable tests in your suite. Create tests/test_logic.py:

from app import compute_next_value

def test_compute_next_value_increments():
    assert compute_next_value(0, 1) == 1
    assert compute_next_value(5, 3) == 8

def test_compute_next_value_handles_negative_step():
    assert compute_next_value(10, -4) == 6

def test_compute_next_value_starts_from_zero():
    assert compute_next_value(0, 0) == 0

Run them with pytest tests/test_logic.py. These tests execute in milliseconds and give you immediate feedback whenever the calculation logic changes.

Component Testing NiceGUI Elements

Component tests verify that NiceGUI elements behave correctly when their callbacks fire. NiceGUI provides a Screen helper in its own test utilities, but for most projects it is simpler to test callbacks directly by constructing elements inside a client context.

Create tests/test_components.py:

import pytest
from nicegui import ui, context
from app import build_page

@pytest.fixture
def client_page():
    """Set up a fresh client context for each test."""
    with context.client.Slot():
        build_page()
        yield context.get_client()

def test_initial_label_value(client_page):
    labels = list(client_page.elements.values())
    text_labels = [el for el in labels if isinstance(el, ui.label)]
    assert text_labels[1].text == "0"

def test_increment_button_updates_label(client_page):
    buttons = [el for el in client_page.elements.values() if isinstance(el, ui.button)]
    increment_btn = buttons[0]
    increment_btn.click()
    labels = [el for el in client_page.elements.values() if isinstance(el, ui.label)]
    assert labels[1].text == "1"

def test_reset_button_resets_label(client_page):
    buttons = [el for el in client_page.elements.values() if isinstance(el, ui.button)]
    buttons[0].click()
    buttons[0].click()
    buttons[1].click()
    labels = [el for el in client_page.elements.values() if isinstance(el, ui.label)]
    assert labels[1].text == "0"

The context.client.Slot() context manager creates an isolated client, so each test gets a fresh page without spinning up a real HTTP server. This is ideal for fast feedback during development.

Integration Testing with Playwright

Integration tests launch the actual NiceGUI server and drive a real browser. This catches issues that component tests miss, such as routing problems, WebSocket disconnects, and CSS layout regressions. Create tests/test_integration.py:

import pytest
from playwright.sync_api import Page, expect
from nicegui.testing import User

from app import main  # adjust import to your entry point

@pytest.fixture
def user():
    user = User()
    yield user

def test_counter_increments(user):
    user.open("/")
    user.should_see("Counter App")
    user.should_see("0")

    user.click("Increment")
    user.should_see("1")

    user.click("Increment")
    user.should_see("2")

def test_counter_resets(user):
    user.open("/")
    user.click("Increment")
    user.click("Increment")
    user.should_see("2")

    user.click("Reset")
    user.should_see("0")

NiceGUI ships a User helper in nicegui.testing that simulates a browser session in pure Python. For tests that require a real browser, use Playwright directly:

import pytest
from playwright.sync_api import Page

@pytest.fixture(scope="session")
def browser_context(playwright):
    browser = playwright.chromium.launch(headless=True)
    context = browser.new_context(base_url="http://localhost:8080")
    yield context
    context.close()
    browser.close()

def test_counter_in_browser(browser_context: Page):
    page = browser_context.new_page()
    page.goto("/")
    page.wait_for_text("Counter App")

    page.get_by_role("button", name="Increment").click()
    page.get_by_role("button", name="Increment").click()
    expect(page.get_by_text("2", exact=True)).to_be_visible()

    page.get_by_role("button", name="Reset").click()
    expect(page.get_by_text("0", exact=True)).to_be_visible()
    page.close()

For Playwright tests to work, your NiceGUI server must be running. You can automate this with a session-scoped fixture that starts the server in a background thread:

import threading
import time
import pytest
from nicegui import ui
import app as nicegui_app

@pytest.fixture(scope="session", autouse=True)
def start_server():
    thread = threading.Thread(target=nicegui_app.ui.run, kwargs={
        "port": 8080, "reload": False, "show": False
    }, daemon=True)
    thread.start()
    time.sleep(2)  # wait for server startup
    yield

Testing Async Event Handlers

NiceGUI event handlers are often asynchronous. With pytest-asyncio, you can test them directly. Suppose you add an async handler that fetches data:

# app.py addition
import asyncio

async def fetch_remote_value() -> int:
    await asyncio.sleep(0.1)
    return 42
# tests/test_async.py
import pytest
from app import fetch_remote_value

async def test_fetch_remote_value():
    result = await fetch_remote_value()
    assert result == 42

Best Practices

Conclusion

Testing NiceGUI applications does not have to be complicated. By separating your business logic into pure functions, using client slots for fast component tests, and reserving Playwright for end-to-end integration tests, you build a layered safety net that scales with your application. Start with unit tests for the logic that matters most, add component tests for interactive elements, and introduce browser-based tests once your user flows stabilize. With this strategy in place, you can iterate on your NiceGUI app with confidence, knowing that regressions will be caught early and automatically.

— Ad —

Google AdSense will appear here after approval

← Back to all articles