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
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 aClockschedule, 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:
- Set up the app instance — create your
Appsubclass, but don't callrun()yet. - 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). - Schedule assertions — after the event, schedule one or more checks to run on later frames.
- Run the event loop — call
runTouchApp(app)(orEventLoop.run()) to actually process events. - 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
Touchinstance withposmatching the button's center, then on the next frame callbutton.on_touch_down(touch)andbutton.on_touch_up(touch). Or useEventLoop.post_dispatch_inputto inject a real input event. - Changing text in a
TextInput— settextdirectly and call theon_textvalidator, or simulate a keyboard event. - Checking widget state — access properties like
text,state,disabled, etc., and use Python'sassert.
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_oncewith a small delay like 0.1 seconds inassert_successif your app has animations or asynchronous logic. You can also callClock.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+Clockfor simple component tests — it's zero-config and teaches the internals. - Move to
pytestwhen you need more tests, fixtures, and CI-friendly reports. - Use a higher-level tool like
kivicatalogorkivy.garden.xpopuponly 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_onceto schedule test steps inside the event loop. - Simulate taps with
MockTouchmatching 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.rootoutside theClock.schedule_oncecallback returnsNone. - Forgetting to call
app.stop()at the end of the test — the event loop never exits and the script hangs. - Dispatching only
on_touch_downwithouton_touch_up, causing buttons that rely onon_releaseto never fire. - Using
EventLoop.run()in Kivy 2.3+ and ignoring deprecation warnings — switch toapp.run()for forward compatibility.
Variations
- Use
pytestwith thepytest-kivyplugin to organize tests and get better assertions and fixtures. - Use
kivy.tests.mock'sMockKeyboardto simulate keyboard input forTextInputvalidation. - Explore the experimental
kivy.unitmodule 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
ClockandEventLooplet you simulate user interactions and assertions inside the app's event loop. - Always schedule test steps with
Clock.schedule_onceto let the UI update before asserting. - Use
MockTouchand 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 deprecatedEventLoop.run()for Kivy 2.3+. - For larger suites, integrate with
pytestand run tests in CI automatically.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.