Функция модульного тестирования Swift, которая асинхронно отправляет блок, задающий переменную, которую я хочу протестировать

#swift #unit-testing

Вопрос:

У меня есть код, который выглядит следующим образом

 class Vibration: NSObject {
    var status: VibrationStatus // an enum
}
 

и функция в другом классе (типа NSObject), подобная следующей, которая является частью объекта, обладающего свойством вибрация типа Вибрация

 func vibrate() {
    DispatchQueue.main.async { [weak self] in
       vibration.status = .vibrating
       // do some real HW vibrate stuff
    }
}

 

Ни одно из определений свойств или классов не включает @objc или @objcMembers

Я пытаюсь создать тест, который будет ждать этого асинхронного вызова, чтобы установить vibration.status.

У меня есть тестовая функция, которая, кажется, работает (см. Ниже), когда я объявляю status свойство как @objc или помещаю @objcMembers в Vibration класс.

 func testVibrate() {
    let invite: SignalingInviteBody = SignalingInviteBody()
    let incomingCall = IncomingCall(invite)
    let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(format: "status = 2"), object: incomingCall.vibration)
    incomingCall.startRing() // this calls the function vibrate()
    wait(for: [expectation], timeout: 3.0)
}
 

Этот тест также требует @objc объявления перечисления с декларацией перечисления, совместимой с Objective-C, чего я не хочу, так как это было бы только для тестирования.

За исключением тестирования, нет необходимости создавать status свойство @objc или Vibration класс как @objcMembers , и я бы предпочел не менять код базовой программы на более неэффективный стиль, когда мне не нужна совместимость с Objective-C в базовой программе.

Есть ли способ проверить это по-настоящему, честно и быстро?

Ответ №1:

Предисловие: это просто какой-то псевдокод, который я быстро набрал в браузере. Вероятно, ему потребуется некоторая полировка, прежде чем он будет правильно скомпилирован.

Я бы использовал макет и инъекцию зависимостей:

 class MockVibration: Vibration {
    let statusChanged: (VibrationStatus) -> Void

    init(statusChanged: (VibrationStatus) -> Void) {
        self.statusChanged = statusChanged
    }

    var status: VibrationStatus {
        didSet {
            statusChanged(status)
        }
    }
}
 

У меня, вероятно, был бы протокол, и я бы Vibration и то, и MockVibration другое соответствовало ему, но наличие MockVibration: Vibration должно хорошо работать. Как только вы определили этот макет, вы можете использовать его для выполнения ожиданий в своем тестовом примере:

 func testVibrate() {
    let didVibrate = self.expectation(description: "Started vibrating")
    let mockVibration = MockVibration { newStatus in 
        XCTAssertEqual(newStatus, .vibrating)
        didVibrate.fulfill()
    }

    let invite = SignalingInviteBody()
    let incomingCall = IncomingCall(invite, mockVibration)
    

    incomingCall.startRing() // this calls the function vibrate()

    wait(for: [didVibrate], timeout: 3.0)
}
 

Возможно, вы даже сможете переработать этот интерфейс так, чтобы это DispatchQueue.main.async {} происходило внутри Vibration класса как внутренняя деталь. Если вы это сделаете, взаимодействие между IncomingCall и Vibration станет синхронным, поэтому вам не нужно будет использовать ожидания. Ваш тест на входящий вызов сократится до:

 class MockVibration {
    // The mock can just have its status set simply/synchronously
    var status: VibrationStatus = .off // or some other "initial" value
}

func testVibrate() {
    let mockVibration = MockVibration()

    let invite = SignalingInviteBody()
    let incomingCall = IncomingCall(invite, mockVibration)
    

    incomingCall.startRing() // this calls the function vibrate()

    XCTAssertEqual(mockVibration.status, .vibrating)
}
 

Конечно, тогда вам понадобится отдельный тест , который охватывает Vibration и гарантирует, что его общедоступные API заставят его изменить свое внутреннее состояние, используя правильную очередь отправки или что-то еще.