Automate UI Testing for Kivy

Learn to automate UI testing for Kivy apps. This hands-on tutorial covers core concepts, step-by-step workflows, practical examples, troubleshooting, and what to study next in the Mobile App Development track.

Focus: automate ui testing for kivy apps

Sponsored

Manually clicking through your Kivy app to verify every screen, button, and text input is tedious, error-prone, and simply doesn't scale as your project grows. Before you know it, you've shipped a bug that slipped through because you missed a tap. In this lesson, you'll learn how to automate UI testing for Kivy apps using the built-in kivy.base.EventLoop and kivy.clock.Clock to simulate user interactions, assert on widget states, and keep your app rock-solid through every iteration.

The Problem This Lesson Solves

When you're building a mobile app with Kivy, your most important user-facing design decisions live in the UI. But verifying the UI manually—launching the app, tapping through every screen, checking if the button changes color, or if the text updates—takes minutes per test run and is impossible to repeat exactly the same way twice. Here's the pain:

  • Regression bugs sneak in silently. A small change to a layout or a callback can break a flow you tested yesterday.
  • Test coverage is shallow. You ship „it looks fine" instead of „it works like I designed."
  • Feedback loop is slow. Every manual check eats time you could spend building features.

Automated UI testing fixes this by letting you script user interactions—taps, text entry, scrolling—and assert on the resulting widget state, all without a real device. It gives you a repeatable, fast, and precise way to protect your app's core flows.

By the end of this lesson, you'll be able to write a small test suite that simulates a user, checks that your widgets respond correctly, and catches regressions before your users do.

Core Concept / Mental Model

Think of automated UI testing for Kivy as rehearsing the user's story. You write a script that acts like a real user: it finds a button, fires a tap event, then waits and checks if the right thing happened. Kivy gives you two essential ingredients to make this work:

  • EventLoop — the heartbeat of the app. It processes input events and updates the UI. Your test runs inside a Clock schedule, so the app's logic is actually executing.
  • Clock — Kivy's scheduler. You can schedule a function to run immediately, after a delay, or repeatedly. This becomes your „time machine" in tests to pause, wait for UI updates, and then assert.

Here's the key insight: your test runs in the same thread as the app's main loop, but you take over the event loop yourself. Instead of calling runTouchApp(), your test creates the app, schedules a test function, and then calls runTouchApp() to execute. That test function drives the UI through the app's own callbacks.

Pro tip: Everything in a Kivy test is asynchronous from the test's perspective. After you trigger a tap, you must schedule the assertion for the next frame (or a few frames later) so the event has time to propagate.

The mental model: test → trigger event → yield to the event loop → assert. You're not actually blocking on time; you're teaming up with Kivy's scheduler.

How It Works Step by Step

Writing an automated UI test for a Kivy app follows the same shape every time:

  1. Set up the app instance — create your App subclass, but don't call run() yet.
  2. Build the test logic — a function that will be scheduled on the Clock. It will: - Find the widget you want to interact with (by id, by class, or by traversing the widget tree). - Simulate an input event (tap, text entry, keyboard).
  3. Schedule assertions — after the event, schedule one or more checks to run on later frames.
  4. Run the event loop — call runTouchApp(app) (or EventLoop.run()) to actually process events.
  5. Stop the loop — once the test asserts and finishes, call app.stop() to exit.

Here's a concrete breakdown of the event simulation:

  • Tapping a button — create a Touch instance with pos matching the button's center, then on the next frame call button.on_touch_down(touch) and button.on_touch_up(touch). Or use EventLoop.post_dispatch_input to inject a real input event.
  • Changing text in a TextInput — set text directly and call the on_text validator, or simulate a keyboard event.
  • Checking widget state — access properties like text, state, disabled, etc., and use Python's assert.

Because Kivy's UI updates on frame boundaries, you'll almost always schedule assertions with Clock.schedule_once with a delay of 0 (next frame) or a small number of frames.

Hands-On Walkthrough

Let's put this into practice. We'll build a tiny Kivy app with a login screen, then write a test that fills a TextInput, taps a button, and checks that a success label appears.

Step 1 — A Simple Login App

Save this as app.py:

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.uix.label import Label

class LoginScreen(BoxLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.orientation = 'vertical'
        self.username = TextInput(hint_text='Username')
        self.password = TextInput(hint_text='Password', password=True)
        self.submit = Button(text='Login', on_press=self.on_login)
        self.status = Label(text='Waiting...')
        self.add_widget(self.username)
        self.add_widget(self.password)
        self.add_widget(self.submit)
        self.add_widget(self.status)

    def on_login(self, instance):
        if self.username.text == 'admin' and self.password.text == 'secret':
            self.status.text = 'Success!'
        else:
            self.status.text = 'Failed!'

class LoginApp(App):
    def build(self):
        return LoginScreen()

if __name__ == '__main__':
    LoginApp().run()

Step 2 — Write the Automated Test

Create test_login.py in the same directory:

from kivy.app import App
from kivy.clock import Clock
from kivy.base import EventLoop
from kivy.tests.mock import MockTouch
from app import LoginApp

def test_login_success():
    app = LoginApp()

    # Prepare the test function that will run inside the event loop?
    def run_test(dt):
        screen = app.root
        # Set credentials
        screen.username.text = 'admin'
        screen.password.text = 'secret'
        # Simulate tapping the submit button
        touch = MockTouch(screen.submit.center)
        screen.submit.on_touch_down(touch)
        screen.submit.on_touch_up(touch)
        # Schedule assertion for next frame
        Clock.schedule_once(assert_success, 0)

    def assert_success(dt):
        screen = app.root
        assert screen.status.text == 'Success!'
        app.stop()  # Stop the loop

    # Schedule the test and run the app
    Clock.schedule_once(run_test, 0)
    EventLoop.run()
    print('Test passed!')

if __name__ == '__main__':
    test_login_success()

Run it with:

python test_login.py

Expected output:

Test passed!

Step 3 — Parametrize and Add a Failure Case

Extend the test to check failed login too:

def test_login_failure():
    app = LoginApp()

    def run_test(dt):
        screen = app.root
        screen.username.text = 'user'
        screen.password.text = 'wrong'
        touch = MockTouch(screen.submit.center)
        screen.submit.on_touch_down(touch)
        screen.submit.on_touch_up(touch)
        Clock.schedule_once(assert_failure, 0)

    def assert_failure(dt):
        assert app.root.status.text == 'Failed!'
        app.stop()

    Clock.schedule_once(run_test, 0)
    EventLoop.run()
    print('Failure test passed!')

if __name__ == '__main__':
    test_login_failure()

Pro tip: For robustness, use Clock.schedule_once with a small delay like 0.1 seconds in assert_success if your app has animations or asynchronous logic. You can also call Clock.tick() repeatedly in a while loop to advance frames manually.

Compare Options / When to Choose What

There's more than one way to test Kivy UIs. Here's a comparison to help you choose:

Approach Pros Cons Best For
kivy.tests + MockTouch Built-in, no extra deps, fast Low-level, verbose Unit tests of specific components
kivy.clock.Clock scheduling Precise control, works without third-party Manual event simulation Quick smoke tests
pytest with kivy-garden Integration with CI, readable syntax Extra dependency, setup overhead Larger test suites
kivy.unit (experimental) Aimed at behavior-driven testing Not stable in older Kivy versions Future-proof projects

When to choose what:

  • Start with MockTouch + Clock for simple component tests — it's zero-config and teaches the internals.
  • Move to pytest when you need more tests, fixtures, and CI-friendly reports.
  • Use a higher-level tool like kivicatalog or kivy.garden.xpopup only if you need advanced interaction simulation (though these are rare for testing).

Troubleshooting & Edge Cases

Test hangs and never finishes

Symptom: The script never prints anything and doesn't exit.

Fix: Ensure you call app.stop() at the end of the final scheduled callback. If you're using EventLoop.run(), it will block until app.stop() is called. Also check that your test function is actually scheduled — if you forget Clock.schedule_once(run_test, 0), nothing happens.

"AttributeError: 'NoneType' object has no attribute 'text'"

Symptom: app.root is None.

Cause: You scheduled your test too early, before Kivy built the widget tree. The build() method is called when runTouchApp starts, not at app instantiation.

Fix: In run_test, access app.root inside the scheduled function, not before scheduling. For example, don't do screen = app.root at the top of the outer function; do it inside run_test.

Touch event doesn't trigger the button

Symptom: The button's callback never fires.

Cause: Your MockTouch position doesn't match the button's bounding box, or you're not dispatching both down and up events.

Fix: Use button.collide_point(*touch.pos) to verify the position. Also, some widgets (like Button with on_release) expect an on_touch_up after on_touch_down. Ensure you call both.

EventLoop.run() is deprecated

Symptom: Warning message about deprecation.

Fix: In Kivy 2.3+, use app.run() instead of EventLoop.run(). Change your call to app.run() and schedule your test before that. For example:

Clock.schedule_once(run_test, 0)
app.run()

UI updates need multiple frames

Symptom: Assertion fails because the label hasn't updated yet.

Fix: Increase the delay in Clock.schedule_once. Use 0.1 seconds or more, or schedule a chain of callbacks that run every frame until the expected condition is met.

What You Learned & What's Next

You can now automate UI testing for Kivy apps by simulating user interactions and verifying widget states. You understand the mental model of driving the event loop, you can write a test that taps buttons and fills inputs, and you know how to debug common issues like hanging loops and None roots.

Key takeaways:

  • Use Clock.schedule_once to schedule test steps inside the event loop.
  • Simulate taps with MockTouch matching the widget's center.
  • Always call app.stop() to exit the loop.
  • Compare options: built-in tools vs. pytest for larger suites.

Next up: In the next lesson, you'll learn about integration testing and setting up a CI pipeline for your Kivy apps. You'll take your automated tests and run them automatically on every commit, so regressions are caught the moment they're introduced.

Practice recap

Write a test for a Kivy app with a simple counter: a button that increments a label. Simulate three taps and assert the label shows '3'. Try automating a text input validation test next, and then move on to integrating your tests into a CI pipeline.

Common mistakes

  • Scheduling the test before the widget tree is built — accessing app.root outside the Clock.schedule_once callback returns None.
  • Forgetting to call app.stop() at the end of the test — the event loop never exits and the script hangs.
  • Dispatching only on_touch_down without on_touch_up, causing buttons that rely on on_release to never fire.
  • Using EventLoop.run() in Kivy 2.3+ and ignoring deprecation warnings — switch to app.run() for forward compatibility.

Variations

  1. Use pytest with the pytest-kivy plugin to organize tests and get better assertions and fixtures.
  2. Use kivy.tests.mock's MockKeyboard to simulate keyboard input for TextInput validation.
  3. Explore the experimental kivy.unit module for a behavior-driven testing approach with dedicated test APIs.

Real-world use cases

  • Automating login flow tests to verify authentication logic in a Kivy app before every release.
  • CI pipeline that runs UI tests on headless Linux servers to catch regressions on every pull request.
  • Testing form validation and error messages on registration screens without manual device testing.

Key takeaways

  • Kivy's Clock and EventLoop let you simulate user interactions and assertions inside the app's event loop.
  • Always schedule test steps with Clock.schedule_once to let the UI update before asserting.
  • Use MockTouch and ensure the touch position collides with the target widget.
  • Call app.stop() to exit the event loop and prevent test hangs.
  • Prefer app.run() over deprecated EventLoop.run() for Kivy 2.3+.
  • For larger suites, integrate with pytest and run tests in CI automatically.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.