Сбор значений Publisher с помощью async

Я написал несколько модульных тестов некоторого кода Combine, который у нас есть. Я столкнулся с некоторыми проблемами. Я думаю, что упростил различные части этого теста. NB: Это не тест — я пытаюсь понять, почему один из тестов не работает!

func test_collectingPassthroughValues() async throws {
    // In the real test this is injected in to the unit under test.
    let subject = PassthroughSubject<Int, Never>()

    // I'm expecting this to only complete once `subject` finishes. I've used
    // `async let` so I can poke some data through `subject` and then later on
    // `await collectValues` to hopefully get back the stuff published by 
    // `subject`. In the real test this is a property from the unit under test
    // which runs various operators on `subject`.
    async let collectValues = await subject.values.reduce(into: []) { $0.append($1) }

    // Send some data through `subject` and then `.finish` it.
    subject.send(10)
    subject.send(20)
    subject.send(completion: .finished)

    // Await the values so we can check we got what's expected.
    let values = await collectValues

    // This fails…
    XCTAssertEqual(values, [10, 20])
}

Утверждение не выполняется с:

est_collectingPassthroughValues(): XCTAssertTrue failed - Found difference for 
Different count:
 |  Received: (0) []
 |  Expected: (2) [10, 20]

Так что subject.values, кажется, вообще ничего не получает; Я не знаю, почему?

Спасибо!

@matt, так что values не «видит» опубликованные значения из subject.send(10), и, я думаю, подбирает только subject.send(completion: .finished)? Есть ли какой-нибудь способ «заставить его начать собирать вещи» и продолжать тестировать вещи, а затем «получать собранные вещи» позже?

Benjohn 17.05.2023 16:29

Извините, я не понял, какова реальная цель теста. Вам не нужно async/await только для того, чтобы протестировать конвейер комбайна. Но я не вижу здесь «настоящего» комбинированного конвейера, который нужно тестировать, поэтому я не понимаю, что вы пытаетесь сделать.

matt 17.05.2023 16:34

@matt Да, извините - это немного сложно передать, поскольку задействованы настоящие конвейеры. В основном, модуль выставляет издатель. Я хочу иметь тесты, которые утверждают, что правильные вещи публикуются на этом, когда я использую модуль с событиями. Я надеялся, что смогу сделать тесты особенно читаемыми с помощью асинхронности, но это может быть не очень хорошим подходом.

Benjohn 17.05.2023 16:41
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
3
61
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Мне кажется, проблема здесь в том, что вы ожидаете, что collectValues будет содержать «10» и «20», которые вы отправляете после установки константы. subject.send(10) добавляет значения к subject.values, а не к collectValues, поэтому тест не пройден.

Изменение такого порядка должно привести к успешному завершению теста.

// Send some data through `subject` and then `.finish` it.
subject.send(10)
subject.send(20)
subject.send(completion: .finished)

// I'm expecting this to only complete once `subject` finishes. I've used
// `async let` so I can poke some data through `subject` and then later on
// `await collectValues` to hopefully get back the stuff published by 
// `subject`. In the real test this is a property from the unit under test
// which runs various operators on `subject`.
async let collectValues = await subject.values.reduce(into: []) { $0.append($1) }
Ответ принят как подходящий

То, что происходит, довольно просто. Как это написать правильно, гораздо менее понятно, и моя рекомендация — «не делайте этого».

Во-первых, небольшая проблема, которая не является проблемой:

async let collectValues = await subject.values.reduce(into: []) { $0.append($1) }

Вы не должны использовать await здесь. Это, вероятно, было бы проблемой, если бы не было других проблем.

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

// Taking out the extra `await`
async let collectValues = subject.values.reduce(into: []) { $0.append($1) }

// That line is pretty close to:
    let collectValues = Task {
        var values: [Int] = []
        for await value in subject.values {
            values.append(value)
        }
        return values
    }

Проблема в том, что это запускает задачу, которая может не начаться сразу. Итак, ваша следующая строка кода subject.send(10) не имеет подписчика (она даже не дошла до строки for-await), и она просто выброшена.

Вы можете исправить это, добавив try await Task.sleep(for: .seconds(1)) после создания задачи, но это мало помогает. На PassthroughSubject нет буферизации. Пока ты звонишь append, тебя никто не слушает. Ценность будет выброшена, и вы сбросите 20.

Вы можете улучшить ситуацию с помощью буферизации, но вам все равно нужно спать (что неприемлемо для IMO). Тем не менее, для меня очень надежным является следующее:

func test_collectingPassthroughValues() async throws {
    // In the real test this is injected in to the unit under test.
    let subject = PassthroughSubject<Int, Never>()
    let readSubject = subject.buffer(size: 10, prefetch: .keepFull, whenFull: .dropOldest)

    async let collectValues = readSubject.values.reduce(into: []) { $0.append($1) }

    try await Task.sleep(for: .seconds(1))
    subject.send(10)
    subject.send(20)
    subject.send(completion: .finished)

    // Await the values so we can check we got what's expected.
    let values = await collectValues

    XCTAssertEqual(values, [10, 20])
}

Но ИМО, это полностью сломанный подход.

Я бы не стал смешивать PassthroughSubject с .values. Я просто не вижу способа сделать его прочным. В более широком смысле я рекомендую очень осторожно смешивать Combine и Structured Concurrency. У них, как правило, очень разные представления о том, как все должно работать.

Привет, Роб, спасибо за отличный информативный и полезный ответ! Очень полезно иметь убедительное объяснение того, почему что-то не работает, и даже если заставить работать, это не очень хороший план! Спасибо 👍

Benjohn 18.05.2023 18:00

Другие вопросы по теме