Как проверить, синхронизирована ли группа ожидания.Функция Done() вызывается в модульном тесте

# #unit-testing #go #synchronization #waitgroup

Вопрос:

Допустим, у меня есть функция, если она выполняется асинхронно как обычная процедура:

 func f(wg *sync.WaitGroup){
    defer wg.Done()
    // Do sth
}

main(){
    var wg sync.WaitGroup
    wg.Add(1)
    go f(amp;wg)
    wg.Wait() // Wait until f is done
    // ...
}
 

Как бы я создал модульный тест f , чтобы убедиться wg.Done() , что он называется?

Один из вариантов-вызвать wg.Done() тест сразу после f вызова. Если f не удастся вызвать wg.Done() тест, начнется паника, что нехорошо. Другим вариантом было бы создать интерфейс для sync.WaitGroup , но это кажется немного странным.

Комментарии:

1. На самом деле, 1.18, похоже, меняет структуру группы ожидания по сравнению с 1.17, поэтому, если вы воспользуетесь приведенным выше предложением, а затем в начале 2021 года обновите ее до 1.18… эти тесты гарантированно взорвутся. 1.17 и текущий мастер

2. Если Done не вызывается, то Ожидание не вернется, и тесты будут нарушены. Вам не нужно проверять внутренние устройства группы ожидания.

3. Потому что именно так вы бы проверили этот код. Нет ничего плохого в использовании гороутины в модульном тесте, который тестирует код, являющийся гороутиной. Параллелизм является основополагающим для использования языка, а не чем-то, предназначенным только для интеграции.

4. @RasmusN вы здесь прогибаетесь назад — сначала, чтобы избежать ошибок в тесте (вероятно, потому, что ваш опыт работы с параллелизмом на других языках позволяет вам думать об этом как о чем-то, что следует избегать в модульных тестах), а затем спорить с JimB вместо того, чтобы учиться на их вкладах. Отпусти, будь собой. Позвольте опытным людям, таким как Джимб, направлять вас.

5. Пока вы не решите проблему остановки, вы не сможете доказать, что какой-либо набор тестов будет завершен. Почему вы планируете, чтобы тест регулярно проваливался? Мы всегда откладываем выполнение в начале функции именно для того, чтобы ее никогда нельзя было пропустить, и ее удаление было бы очевидно при просмотре. Если бы вы каким-то образом пропустили его, время выполнения теста истекло один раз, и вы это исправили.

Ответ №1:

Как бы я создал модульный тест для f, который гарантирует, что wg.Done() будет вызван?

Что-то вроде этого:

 func TestF(t *testing.T) {
    wg := amp;sync.WaitGroup{}
    wg.Add(1)

    // run the task asynchronously
    go f(wg)

    // wait for the WaitGroup to be done, or timeout
    select {
    case <-wrapWait(wg):
        // all good
    case <-time.NewTimer(500 * time.Millisecond).C:
        t.Fail()
    }
}

// helper function to allow using WaitGroup in a select
func wrapWait(wg *sync.WaitGroup) <-chan struct{} {
    out := make(chan struct{})
    go func() {
        wg.Wait()
        out <- struct{}{}
    }()
    return out
}
 

Вы не проверяете группу ожидания напрямую, чего вы все равно не можете сделать. Вместо этого вы утверждаете, что функция ведет себя так, как ожидалось, с учетом ожидаемых входных данных.

В этом случае ожидаемый ввод-это аргумент группы ожидания, и ожидаемое поведение-это wg.Done() то, что в конечном итоге вызывается. Что это означает на практике? Это означает, что в случае успешного выполнения функции группа ожидания со счетом 1 достигнет 0 и позволит wg.Wait() продолжить.

Инструкция defer wg.Done() в начале f уже гарантирует, что тест устойчив к ошибкам или сбоям. Добавление тайм — аута просто для того, чтобы убедиться, что тест завершится в разумные сроки, т. Е. Что он не задержит ваш набор тестов слишком надолго. Лично я предпочитаю использовать явные тайм-ауты, либо с помощью таймеров, либо с контекстами, чтобы 1) избежать проблем, если кто-то забудет установить тайм-ауты на уровне CI, 2) сделать ограничение по времени доступным для всех, кто проверяет репозиторий и запускает набор тестов, т. Е. Избегать зависимостей от конфигураций IDE или чего-то еще.

Комментарии:

1. вместо того, чтобы отправлять что-то на out канал, вы также можете просто закрыть его