Modern QA2026Тестирование ML-моделей на устройстве
Join

Course09 Mobile & Cross-Platform Testing

Cutting-edge · Chapter 09

Тестирование ML-моделей на устройстве

Updated Jul 2026

Почему ML на устройстве требует отдельного тестирования

Приложения всё чаще запускают ML-модели непосредственно на устройстве (Core ML на iOS, TensorFlow Lite на Android) для таких функций, как классификация изображений, предсказание текста, детекция объектов и распознавание лиц. Эти модели выполняются локально без сетевого подключения, обеспечивая более быстрый инференс и лучшую конфиденциальность.

Однако ML на устройстве создаёт вызовы тестирования, которых нет у серверного ML. Модель должна загружаться в пределах ограничений памяти, инференс должен завершаться в рамках бюджета задержки, и модель должна обеспечивать приемлемую точность на оборудовании с ограниченной вычислительной мощностью. Модель, которая отлично работает на сервере с GPU, может показывать низкую производительность на смартфоне среднего класса.

Что тестировать для ML на устройстве

Область тестирования Что проверять Пример утверждения
Загрузка модели Модель инициализируется без краша на целевых устройствах Время загрузки < 2 с на устройствах Уровня 1
Задержка инференса Предсказание завершается за приемлемое время p95 < 100 мс на устройствах среднего класса
Точность на устройстве Модель даёт те же результаты, что и серверная Совпадение > 99% на валидационном наборе
Потребление памяти Модель не вызывает OOM на устройствах с ограниченными ресурсами Пиковое потребление < 100 МБ
Влияние на батарею Непрерывный инференс не разряжает батарею < 5% в час активного использования
Поведение при откате Приложение работает, когда модель не загружается Корректная деградация до серверного API
Обновление модели OTA-обновления модели применяются корректно Новая версия модели загружается после перезапуска приложения
Параллельный инференс Множественные запросы инференса не вызывают краш 10 параллельных запросов завершаются

Тестирование задержки инференса

# tests/ml/test_ondevice_model.py
import time
from appium.webdriver.common.appiumby import AppiumBy

def test_image_classification_latency(driver):
    """On-device ML inference must complete within 200ms."""
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "ml-classify-btn").click()

    # Inject test image
    driver.push_file("/sdcard/test_images/cat.jpg", source_path="test_data/cat.jpg")

    start = time.time()
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "classify-image").click()

    # Wait for result
    result = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "classification-result")
    elapsed = time.time() - start

    assert elapsed < 0.2, f"Inference took {elapsed:.3f}s, exceeding 200ms threshold"
    assert "cat" in result.text.lower()

def test_inference_latency_p95(driver):
    """Measure p95 inference latency across multiple runs."""
    latencies = []

    for i in range(20):
        driver.push_file(
            f"/sdcard/test_images/test_{i}.jpg",
            source_path=f"test_data/validation/image_{i}.jpg"
        )

        start = time.time()
        driver.find_element(AppiumBy.ACCESSIBILITY_ID, "classify-image").click()
        driver.find_element(AppiumBy.ACCESSIBILITY_ID, "classification-result")
        elapsed = (time.time() - start) * 1000  # Convert to ms
        latencies.append(elapsed)

        # Reset for next image
        driver.find_element(AppiumBy.ACCESSIBILITY_ID, "clear-result").click()

    latencies.sort()
    p95 = latencies[int(len(latencies) * 0.95)]

    assert p95 < 200, f"p95 inference latency is {p95:.0f}ms, exceeding 200ms threshold"

Тестирование точности модели на устройстве

Одна и та же модель может давать слегка различающиеся результаты на разном оборудовании из-за различий точности вычислений с плавающей запятой между GPU, CPU и NPU инференсом.

def test_model_accuracy_matches_server(driver):
    """On-device predictions must agree with server-side predictions."""
    validation_set = load_validation_set("test_data/validation/")
    agreements = 0
    total = len(validation_set)

    for image_path, expected_label in validation_set:
        # Get on-device prediction
        driver.push_file("/sdcard/test_images/current.jpg", source_path=image_path)
        driver.find_element(AppiumBy.ACCESSIBILITY_ID, "classify-image").click()
        result = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "classification-result")
        device_prediction = result.text

        # Compare with server-side prediction
        if device_prediction.lower() == expected_label.lower():
            agreements += 1

        driver.find_element(AppiumBy.ACCESSIBILITY_ID, "clear-result").click()

    accuracy = agreements / total
    assert accuracy >= 0.99, \
        f"On-device accuracy {accuracy:.2%} is below 99% threshold"

def test_model_handles_edge_case_inputs(driver):
    """Model should handle adversarial and edge case inputs gracefully."""
    edge_cases = [
        ("test_data/blank_white.jpg", "no_object_detected"),
        ("test_data/blank_black.jpg", "no_object_detected"),
        ("test_data/tiny_1x1.jpg", "invalid_input"),
        ("test_data/very_large.jpg", "cat"),  # Should still classify correctly
        ("test_data/rotated_90.jpg", "cat"),  # Rotation invariance
        ("test_data/low_quality.jpg", "cat"),  # JPEG quality = 10
    ]

    for image_path, expected_category in edge_cases:
        driver.push_file("/sdcard/test_images/edge.jpg", source_path=image_path)
        driver.find_element(AppiumBy.ACCESSIBILITY_ID, "classify-image").click()

        # Should not crash regardless of input
        result = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "classification-result")
        assert result.is_displayed(), f"Model crashed on {image_path}"

        driver.find_element(AppiumBy.ACCESSIBILITY_ID, "clear-result").click()

Тестирование поведения при откате

Когда модель на устройстве не загружается (повреждённый файл, неподдерживаемое устройство, недостаточно памяти), приложение должно корректно деградировать до серверного API.

def test_model_fallback_when_unavailable(driver):
    """App should fall back to server-side API when model fails to load."""
    # Simulate model file corruption
    driver.execute_script("mobile: shell", {
        "command": "rm",
        "args": ["/data/data/com.app/files/ml_model.tflite"]
    })

    # Restart app
    driver.terminate_app("com.app")
    driver.activate_app("com.app")

    # Feature should still work (via server fallback)
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "classify-image").click()
    result = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "classification-result")
    assert result.is_displayed()

    # Verify fallback indicator is shown
    fallback_badge = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "server-mode-indicator")
    assert fallback_badge.is_displayed()

def test_fallback_to_server_on_low_memory(driver):
    """On low-memory devices, the app should use server-side inference."""
    # Get available memory
    memory_info = driver.execute_script("mobile: shell", {
        "command": "cat",
        "args": ["/proc/meminfo"]
    })

    # If available memory is under threshold, model may not load
    # The app should detect this and use server-side inference
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "classify-image").click()
    result = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "classification-result")
    assert result.is_displayed()  # Feature works regardless of inference path

Тестирование OTA-обновлений модели

def test_model_update_applies_correctly(driver):
    """Over-the-air model updates should apply on next app launch."""
    # Get current model version
    model_info = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "model-version")
    original_version = model_info.text

    # Trigger model update check
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "check-model-update").click()

    # Wait for download
    import time
    time.sleep(10)

    # Restart app to load new model
    driver.terminate_app("com.app")
    driver.activate_app("com.app")

    # Verify new model version is loaded
    model_info = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "model-version")
    new_version = model_info.text

    # Version should be different (assuming an update was available)
    # In CI, you can stage a test model update
    assert new_version != original_version or new_version == "latest"

Тестирование влияния на память и батарею

def test_memory_usage_during_inference(driver):
    """Continuous inference must not cause memory growth."""
    # Get initial memory usage
    initial_memory = get_app_memory(driver)

    # Run 50 consecutive inferences
    for i in range(50):
        driver.find_element(AppiumBy.ACCESSIBILITY_ID, "classify-image").click()
        driver.find_element(AppiumBy.ACCESSIBILITY_ID, "classification-result")
        driver.find_element(AppiumBy.ACCESSIBILITY_ID, "clear-result").click()

    # Get final memory usage
    final_memory = get_app_memory(driver)

    # Memory should not grow significantly (allow 20% for caching)
    memory_growth = (final_memory - initial_memory) / initial_memory
    assert memory_growth < 0.20, \
        f"Memory grew by {memory_growth:.1%} during sustained inference"

def get_app_memory(driver):
    """Get the current memory usage of the app in MB."""
    result = driver.execute_script("mobile: shell", {
        "command": "dumpsys",
        "args": ["meminfo", "com.app", "--short"]
    })
    # Parse total PSS from output
    import re
    match = re.search(r'TOTAL\s+(\d+)', result)
    return int(match.group(1)) / 1024 if match else 0

Тестирование ML на устройстве требует сочетания традиционного функционального тестирования (даёт ли модель правильный результат?) и тестирования производительности (делает ли она это достаточно быстро, в рамках ограничений памяти, не разряжая батарею?). Поведение при откате — наиболее критичный тест: если модель не работает, пользователь всё равно должен иметь возможность использовать функцию.