Skip to content

Latest commit

 

History

History
300 lines (222 loc) · 7.53 KB

File metadata and controls

300 lines (222 loc) · 7.53 KB

MicroPythonOS Testing Guide

This directory contains the test suite for MicroPythonOS. Tests can run on both desktop (for fast iteration) and on-device (for hardware verification).

Quick Start

# Run all tests
./tests/unittest.sh

# Run a specific test
./tests/unittest.sh tests/test_graphical_keyboard_q_button_bug.py

# Run on device
./tests/unittest.sh tests/test_graphical_keyboard_q_button_bug.py --ondevice

Test Architecture

Directory Structure

tests/
├── base/                    # Base test classes (DRY patterns)
│   ├── __init__.py         # Exports GraphicalTestBase, KeyboardTestBase
│   ├── graphical_test_base.py
│   └── keyboard_test_base.py
├── screenshots/             # Captured screenshots for visual regression
├── test_*.py               # Test files
├── unittest.sh             # Test runner script
└── README.md               # This file

Testing Modules

MicroPythonOS provides two testing modules:

  1. mpos.testing - Hardware and system mocks

    • Location: internal_filesystem/lib/mpos/testing/
    • Use for: Mocking hardware (Pin, PWM, I2S, NeoPixel), network, async operations
  2. mpos.ui.testing - LVGL/UI testing utilities

    • Location: internal_filesystem/lib/mpos/ui/testing.py
    • Use for: UI interaction, screenshots, widget inspection

Base Test Classes

GraphicalTestBase

Base class for all graphical (LVGL) tests. Provides:

  • Automatic screen creation/cleanup
  • Screenshot capture
  • Widget finding utilities
  • Custom assertions
from base import GraphicalTestBase

class TestMyUI(GraphicalTestBase):
    def test_something(self):
        # self.screen is already created
        label = lv.label(self.screen)
        label.set_text("Hello")
        
        self.wait_for_render()
        self.assertTextPresent("Hello")
        self.capture_screenshot("my_test.raw")

Key Methods:

  • wait_for_render(iterations=5) - Process LVGL tasks
  • capture_screenshot(filename) - Save screenshot
  • find_label_with_text(text) - Find label widget
  • click_button(button) - Simulate button click
  • assertTextPresent(text) - Assert text is on screen
  • assertWidgetVisible(widget) - Assert widget is visible

KeyboardTestBase

Extends GraphicalTestBase for keyboard tests. Provides:

  • Keyboard and textarea creation
  • Reliable keyboard button clicking
  • Textarea assertions
from base import KeyboardTestBase

class TestMyKeyboard(KeyboardTestBase):
    def test_typing(self):
        self.create_keyboard_scene()
        
        self.click_keyboard_button("h")
        self.click_keyboard_button("i")
        
        self.assertTextareaText("hi")

Key Methods:

  • create_keyboard_scene() - Create textarea + MposKeyboard
  • click_keyboard_button(text) - Click keyboard button reliably
  • type_text(text) - Type a string
  • get_textarea_text() - Get textarea content
  • clear_textarea() - Clear textarea
  • assertTextareaText(expected) - Assert textarea content
  • assertTextareaEmpty() - Assert textarea is empty

Mock Classes

Import mocks from mpos.testing:

from mpos.testing import (
    # Hardware mocks
    MockMachine,      # Full machine module mock
    MockPin,          # GPIO pins
    MockPWM,          # PWM for buzzer
    MockI2S,          # Audio I2S
    MockTimer,        # Hardware timers
    MockNeoPixel,     # LED strips
    MockSocket,       # Network sockets
    
    # MPOS mocks
    MockTaskManager,  # Async task management
    MockDownloadManager,  # HTTP downloads
    
    # Network mocks
    MockNetwork,      # WiFi/network module
    MockRequests,     # HTTP requests
    MockResponse,     # HTTP responses
    
    # Utility mocks
    MockTime,         # Time functions
    MockJSON,         # JSON parsing
    
    # Helpers
    inject_mocks,     # Inject mocks into sys.modules
    create_mock_module,  # Create mock module
)

Injecting Mocks

from mpos.testing import inject_mocks, MockMachine, MockNetwork

# Inject before importing modules that use hardware
inject_mocks({
    'machine': MockMachine(),
    'network': MockNetwork(connected=True),
})

# Now import the module under test
from mpos.hardware import some_module

Mock Examples

MockNeoPixel:

from mpos.testing import MockNeoPixel, MockPin

pin = MockPin(5)
leds = MockNeoPixel(pin, 10)

leds[0] = (255, 0, 0)  # Set first LED to red
leds.write()

assert leds.write_count == 1
assert leds[0] == (255, 0, 0)

MockRequests:

from mpos.testing import MockRequests

mock_requests = MockRequests()
mock_requests.set_next_response(
    status_code=200,
    text='{"status": "ok"}',
    headers={'Content-Type': 'application/json'}
)

response = mock_requests.get("https://api.example.com/data")
assert response.status_code == 200

MockTimer:

from mpos.testing import MockTimer

timer = MockTimer(0)
timer.init(period=1000, mode=MockTimer.PERIODIC, callback=my_callback)

# Manually trigger for testing
timer.trigger()

# Or trigger all timers
MockTimer.trigger_all()

Test Naming Conventions

  • test_*.py - Standard unit tests
  • test_graphical_*.py - Tests requiring LVGL/UI (detected by unittest.sh)
  • manual_test_*.py - Manual tests (not run automatically)

Writing New Tests

Simple Unit Test

import unittest

class TestMyFeature(unittest.TestCase):
    def test_something(self):
        result = my_function()
        self.assertEqual(result, expected)

Graphical Test

from base import GraphicalTestBase
import lvgl as lv

class TestMyUI(GraphicalTestBase):
    def test_button_click(self):
        button = lv.button(self.screen)
        label = lv.label(button)
        label.set_text("Click Me")
        
        self.wait_for_render()
        self.click_button(button)
        
        # Verify result

Keyboard Test

from base import KeyboardTestBase

class TestMyKeyboard(KeyboardTestBase):
    def test_input(self):
        self.create_keyboard_scene()
        
        self.type_text("hello")
        self.assertTextareaText("hello")
        
        self.click_keyboard_button("Enter")

Test with Mocks

import unittest
from mpos.testing import MockNetwork, inject_mocks

class TestNetworkFeature(unittest.TestCase):
    def setUp(self):
        self.mock_network = MockNetwork(connected=True)
        inject_mocks({'network': self.mock_network})
    
    def test_connected(self):
        from my_module import check_connection
        self.assertTrue(check_connection())
    
    def test_disconnected(self):
        self.mock_network.set_connected(False)
        from my_module import check_connection
        self.assertFalse(check_connection())

Best Practices

  1. Use base classes - Extend GraphicalTestBase or KeyboardTestBase for UI tests
  2. Use mpos.testing mocks - Don't create inline mocks; use the centralized ones
  3. Clean up in tearDown - Base classes handle this, but custom tests should clean up
  4. Don't include if __name__ == '__main__' - The test runner handles this
  5. Use descriptive test names - test_keyboard_q_button_works not test_1
  6. Add docstrings - Explain what the test verifies and why

Debugging Tests

# Run with verbose output
./tests/unittest.sh tests/test_my_test.py

# Run with GDB (desktop only)
gdb --args ./lvgl_micropython/build/lvgl_micropy_unix -X heapsize=8M tests/test_my_test.py

Screenshots

Screenshots are saved to tests/screenshots/ in raw format. Convert to PNG:

cd tests/screenshots
./convert_to_png.sh