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