Skip to content

Instantly share code, notes, and snippets.

@howardrotterdam
Created March 26, 2025 11:57
Show Gist options
  • Select an option

  • Save howardrotterdam/63c06754d9f7e1b1fa2c390be6319a44 to your computer and use it in GitHub Desktop.

Select an option

Save howardrotterdam/63c06754d9f7e1b1fa2c390be6319a44 to your computer and use it in GitHub Desktop.
How to do automated test of non-web GUI applications written in eg. tkinter or pyqt headless in a CI/CD pipeline?

How to do automated test of non-web GUI applications written in eg. tkinter or pyqt headless in a CI/CD pipeline?

Grok3:

Automating tests for non-web GUI applications (like those built with Tkinter or PyQt) in a headless environment within a CI/CD pipeline can be challenging because these frameworks are designed to interact with a graphical interface, and headless systems (like CI servers) typically lack a display. However, there are strategies and tools to make this possible. Here's a step-by-step approach:


1. Understand the Challenges

  • Headless Environment: CI/CD systems (e.g., GitHub Actions, Jenkins, GitLab CI) often run on servers without a display, while Tkinter and PyQt expect a graphical environment (X11 on Linux, Windows GUI, etc.).
  • Event Simulation: GUI testing requires simulating user interactions (clicks, key presses, etc.).
  • Dependencies: GUI frameworks may require additional setup (e.g., Xvfb for virtual displays on Linux).

2. Set Up a Virtual Display

To run GUI applications headlessly, use a virtual framebuffer or display server:

  • Xvfb (X Virtual Framebuffer): On Linux-based CI systems, Xvfb emulates a display, allowing GUI apps to run without a physical screen.
    • Install: sudo apt-get install xvfb (Ubuntu/Debian) or equivalent.
    • Run tests with: xvfb-run python -m pytest your_test_file.py.
  • Alternatives:
    • Xvnc: A VNC server for remote display.
    • Headless Windows/macOS: For Windows/macOS CI runners, tools like pywin32 or a full desktop environment may be needed (less common in CI).

3. Choose a Testing Framework

Select a framework that supports GUI automation and integrates with your CI/CD pipeline:

  • PyTest: A general-purpose testing framework; pair it with GUI-specific libraries.
  • unittest: Python’s built-in testing module, though less flexible than PyTest.
  • tox: For testing across multiple Python versions/environments.

4. GUI Automation Libraries

Use libraries to simulate user interactions with Tkinter or PyQt apps:

  • For Tkinter:
    • tkinter-testing: Libraries like tkinter.ttk or custom wrappers can help, but Tkinter lacks robust built-in testing tools.
    • PyAutoGUI: Simulates mouse clicks and keyboard input. Works cross-platform but is slower and less precise (relies on screen coordinates).
      • Example: pyautogui.click(x, y) or pyautogui.typewrite("text").
    • unittest.mock: Mock Tkinter widgets and methods for unit tests if you avoid full GUI rendering.
  • For PyQt:
    • pytest-qt: A PyTest plugin designed for testing PyQt and PySide applications. It integrates with Qt’s event loop and allows interaction with widgets.
      • Install: pip install pytest-qt.
      • Example:
        def test_button_click(qtbot):
            app = MyQtApp()
            qtbot.addWidget(app)
            qtbot.mouseClick(app.button, QtCore.Qt.LeftButton)
            assert app.label.text() == "Clicked!"
    • QTest (Qt’s built-in): PyQt exposes Qt’s QTest module for simulating events.
      • Example: QTest.mouseClick(widget, Qt.LeftButton).

5. Structure Your Tests

  • Unit Tests: Test individual functions or logic without launching the GUI (mock GUI components if possible).
  • Integration Tests: Test the full GUI app with simulated user interactions.
  • Modularize: Separate GUI code from logic to minimize the need for GUI testing.

Example (Tkinter with PyAutoGUI):

import tkinter as tk
import pyautogui
import time

def test_tkinter_app():
    root = tk.Tk()
    entry = tk.Entry(root)
    entry.pack()
    button = tk.Button(root, text="Submit", command=lambda: root.quit())
    button.pack()
    root.update()  # Force GUI update
    time.sleep(1)  # Wait for GUI to render
    pyautogui.typewrite("Hello")
    pyautogui.click(button.winfo_rootx() + 10, button.winfo_rooty() + 10)
    root.mainloop()
    assert entry.get() == "Hello"

6. Integrate into CI/CD

Here’s how to set it up in popular CI systems:

GitHub Actions (Linux)

name: CI
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y xvfb libxkbcommon-x11-0
          pip install pytest pytest-qt pyautogui
      - name: Run tests
        run: |
          xvfb-run --auto-servernum python -m pytest

GitLab CI

test:
  image: python:3.11
  before_script:
    - apt-get update && apt-get install -y xvfb
    - pip install pytest pytest-qt pyautogui
  script:
    - xvfb-run pytest

Windows/macOS Runners

  • Use runners with a desktop environment (e.g., windows-latest in GitHub Actions).
  • Install PyAutoGUI or pytest-qt directly; no Xvfb needed.

7. Best Practices

  • Minimize GUI Testing: Focus on unit tests for logic; limit GUI tests to critical workflows.
  • Stabilize Tests: GUI tests can be flaky due to timing issues. Add delays (time.sleep()) or wait conditions (e.g., qtbot.waitUntil()).
  • Screenshots: Capture screenshots on failure for debugging (e.g., pyautogui.screenshot()).
  • Mock External Dependencies: Avoid relying on real network calls or file I/O during GUI tests.

8. Alternative Approaches

  • Headless Qt: For PyQt, explore QApplication in offscreen mode (Qt’s Offscreen platform plugin), though this is experimental and limited.
  • Rewrite for CLI: If feasible, refactor the app to separate GUI from logic, allowing CLI-based testing.

This approach should get you started with headless GUI testing in a CI/CD pipeline. For Tkinter, PyAutoGUI with Xvfb is a practical choice; for PyQt, pytest-qt is the go-to solution. Let me know if you need help tailoring this to a specific setup!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment