Как тестировать прерывания звука и смену выхода в iOS

Как тестировать прерывания звука и смену выхода в iOS

Плеер проходит автоматические тесты на облачном Mac, но после отключения наушников продолжает играть через динамик. Возможно, проблема не в декодере: отдельно не проверено, что плеер должен делать при смене аудиовыхода. Системные прерывания звука требуют такого же подхода. Завершение прерывания ещё не означает, что пользователь хочет возобновить воспроизведение. Если отделить системные уведомления от решений плеера, эту логику можно надёжно проверять на сборочном узле без наушников.

Сначала определите события и ожидаемое поведение

Прежде чем писать обработчики уведомлений, сформулируйте правила продукта. Здесь используется осторожный подход: при прерывании воспроизведение приостанавливается, а состояние «играло до прерывания» запоминается. Возобновить воспроизведение можно только тогда, когда уведомление о завершении разрешает это. Если во время прерывания пользователь сам нажал паузу, прежнее намерение возобновить воспроизведение отменяется. Отключение устройства вывода тоже сразу ставит воспроизведение на паузу: по названию нового маршрута нельзя судить, хочет ли пользователь продолжать слушать через динамик.

Входное событие Состояние воспроизведения Намерение возобновить
Прерывание началось во время воспроизведения Пауза Запомнить, что воспроизведение шло
Прерывание завершилось, возобновление разрешено Определяется сохранённым намерением Очистить
Пользователь сам поставил на паузу Пауза Очистить
Прежнее устройство вывода недоступно Пауза Очистить

«Прежнее устройство вывода недоступно» — конкретная причина события, а не название для любой смены аудиомаршрута. Например, при переключении на другое уже подключённое устройство нельзя без отдельного продуктового решения применять правило отключения наушников.

Изолируйте решения плеера в чистом автомате состояний

Разбор уведомлений оставьте адаптеру, а в основную логику передавайте только однозначные события. Следующие типы можно добавить в таргет приложения и импортировать в тестовый таргет. isPlaying обозначает желаемое состояние воспроизведения; фактические вызовы плеера должны выполняться внешним слоем при изменении состояния.

enum AudioEvent {
    case interruptionBegan
    case interruptionEnded(shouldResume: Bool)
    case outputDisconnected
    case userPaused
}

struct PlaybackState {
    var isPlaying = false
    var resumeAfterInterruption = false

    mutating func apply(_ event: AudioEvent) {
        switch event {
        case .interruptionBegan:
            resumeAfterInterruption = isPlaying
            isPlaying = false
        case .interruptionEnded(let shouldResume):
            isPlaying = resumeAfterInterruption && shouldResume
            resumeAfterInterruption = false
        case .outputDisconnected, .userPaused:
            isPlaying = false
            resumeAfterInterruption = false
        }
    }
}

Этот пример охватывает только одно прерывание. Если приложение одновременно ведёт несколько сеансов воспроизведения или поддерживает очередь, с которой можно работать во время паузы, сначала расширьте модель состояния, а затем определите порядок событий. Не пытайтесь выразить всю семантику сеансов одним логическим значением.

Преобразуйте системные уведомления в события

Адаптер подписывается на AVAudioSession.interruptionNotification и считывает тип прерывания из userInfo. При завершении он считывает опцию возобновления и передаёт результат автомату состояний. Для AVAudioSession.routeChangeNotification нужно проверять причину: только oldDeviceUnavailable преобразуется в outputDisconnected. Отсутствующие или неизвестные поля следует регистрировать для последующего разбора, а не самовольно трактовать как разрешение возобновить воспроизведение.

Колбэки уведомлений и действия в интерфейсе могут поступать из разных контекстов выполнения. Направляйте события последовательно одному владельцу состояния и вызывайте паузу или воспроизведение по результату сравнения старого и нового состояний. Иначе при одновременном поступлении «пользователь нажал паузу» и «прерывание завершилось» случайный порядок колбэков может переопределить намерение пользователя.

Запустите тесты граничных случаев на сборочном узле

Сначала проверьте в XCTest два сценария, которые легко упустить: плеер, простаивавший до прерывания, не должен запускаться сам; пауза, нажатая пользователем во время прерывания, должна отменять автоматическое возобновление. Тестам не нужны настоящие наушники или системные уведомления с заданным временем доставки.

import XCTest

final class PlaybackStateTests: XCTestCase {
    func testInterruptionDoesNotStartIdlePlayer() {
        var state = PlaybackState()
        state.apply(.interruptionBegan)
        state.apply(.interruptionEnded(shouldResume: true))
        XCTAssertFalse(state.isPlaying)
    }

    func testUserPauseCancelsResume() {
        var state = PlaybackState(isPlaying: true)
        state.apply(.interruptionBegan)
        state.apply(.userPaused)
        state.apply(.interruptionEnded(shouldResume: true))
        XCTAssertFalse(state.isPlaying)
    }

    func testDisconnectedOutputStopsPlayback() {
        var state = PlaybackState(isPlaying: true)
        state.apply(.outputDisconnected)
        XCTAssertFalse(state.isPlaying)
        XCTAssertFalse(state.resumeAfterInterruption)
    }
}

На облачном Mac сначала проверьте проект и схему командой xcodebuild -list -project App.xcodeproj, затем запустите xcodebuild test -project App.xcodeproj -scheme App -destination 'platform=iOS Simulator,name=<已安装的设备名称>' с фактически используемой общей схемой команды. Имя проекта, схему и название устройства в примере необходимо заменить; список доступных устройств покажет xcrun simctl list devices available. Тестовый таргет должен иметь доступ к типам автомата состояний, а схема — включать этот таргет.

Успешные модульные тесты подтверждают только то, что заданная последовательность событий приводит к ожидаемому решению. Они не доказывают, что системное уведомление обязательно придёт или что звук действительно пойдёт через нужное устройство.

Дополните проверку испытанием на устройстве

На реальном устройстве выполните как минимум четыре проверки: отключите наушники во время воспроизведения и убедитесь, что динамик не подхватил звук неожиданно; вызовите управляемое системное прерывание звука и проверьте уведомления о его начале и завершении; во время прерывания вручную поставьте воспроизведение на паузу и убедитесь, что оно не возобновилось после завершения; снова подключите устройство вывода и проверьте, что одно лишь подключение не запускает воспроизведение. Достаточно записать тип полученного события, его причину и итоговое состояние воспроизведения. Не добавляйте в логи пользовательские данные или учётные данные для доступа.

Если модульные тесты проходят, а на устройстве остаётся ошибка, сначала проверьте, действует ли подписка на уведомления на протяжении жизненного цикла сеанса. Затем проверьте разбор полей уведомлений, порядок доставки событий и синхронизацию фактического состояния плеера с PlaybackState. Особенно важно различать «автомат состояний требует паузы» и «плеер действительно приостановился»: первое покрывают тесты, второе требует проверки адаптера и испытания на устройстве.

Сохраните проверки в процессе слияния изменений

Перед слиянием убедитесь, что тесты автомата состояний охватывают простой, прерывание, паузу по инициативе пользователя и отключение устройства; адаптер уведомлений осторожно обрабатывает неизвестные поля; все события проходят через один последовательный путь выполнения; команда зафиксировала результат как минимум одной проверки на целевом устройстве. Облачный Mac удобен для многократного запуска детерминированных тестов состояния, но фактический аудиомаршрут нужно проверять на устройстве. Разделение этих уровней проверки помогает при сбое понять, где искать причину: в правилах продукта, преобразовании уведомлений или самом выводе звука.

Часто задаваемые вопросы

Достаточно ли теста в симуляторе для проверки отключения наушников?

Нет. Он проверяет решение приложения после заданного события; доставку уведомления и реальный аудиовыход следует проверить на устройстве.

Нужно ли всегда возобновлять звук после прерывания?

Нет. Учитывайте состояние до прерывания, разрешение на возобновление и действия пользователя за время паузы.

UDPMac — облачный Mac

Перенесите следующую сборку Xcode на выделенный физический сервер.

Выберите модель Apple Silicon, регион и срок аренды, а затем запускайте собственный процесс сборки через удалённое подключение. Фактическая доступность отображается в консоли при оформлении заказа.

Посмотреть тарифы на облачный Mac