Синтетическое время для тестов

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

func Read(in chan int) (int, error) {
    select {
    case v := <-in:
        return v, nil
    case <-time.After(60 * time.Second):
        return 0, errors.New("timeout")
    }
}

Используем так:

ch := make(chan int)
go func() {
    ch <- 42
}()

val, err := Read(ch)
fmt.Printf("val=%v, err=%v\n", val, err)
// val=42, err=<nil>

Как бы теперь протестировать ситуацию с таймаутом? Нежелательно, чтобы тест действительно ждал 60 секунд. Можно сделать таймаут параметром функции (и стоило бы, наверно). Но допустим, что это не вариант.

Новый пакет testing/synctest спешит на помощь! Функция synctest.Run выполняет изолированный «пузырь» в отдельной горутине. Внутри пузыря функции пакета time используют искусственные часы, что позволяет тесту пройти мгновенно:

func TestReadTimeout(t *testing.T) {
    synctest.Run(func() {
        ch := make(chan int)
        _, err := Read(ch)
        if err == nil {
            t.Fatal("expected timeout error, got nil")
        }
    })
}

Горутины в пузыре используют синтетическую реализацию времени (точка отсчета - полночь UTC 2000-01-01). Время идет вперед, когда все горутины в пузыре заблокированы.

В нашем тесте, когда единственная горутина заблокировалась на select в Read, часы в пузыре разом скакнули вперед на 60 секунд, и сработал таймаут.

Пакет synctest пока экспериментальный, по умолчанию выключен. Можно включить переменной GOEXPERIMENT=synctest при сборке. API пакета может измениться в будущих версиях.

★ Подписывайтесь на новые заметки.