Как тестировать конвейеры каналов в Go

Aug 15 2020

Я довольно часто использую в Go шаблон «конвейер каналов», который выглядит примерно так:

// getSomeNums spits out ints onto a channel. Temperatures, pressures, doesn't matter
func getSomeNums(ch chan<- int) {
    // Imagination goes here
}

// double takes numbers from in, doubles them, and pushes them into out
func double(in <-chan int, out chan<- int) {
    for v := range in {
        out <- v * 2
    close(out)
}

source := make(chan int)
go getSomeNums(source)

doubles := make(chan int)
double(source, doubles)

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

  • Помещает значение в выходной канал, когда вы помещаете его во входной канал
  • Не помещает значение в выходной канал, если вы не помещаете его во входной канал
  • Время истекает, если выходной канал занимает слишком много времени после появления значения во входном канале
  • Закрывает выходной канал при закрытии входного канала
  • Не закрывает выходной канал до закрытия входного канала
  • Выполняет правильное преобразование на входе

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

// Provided we have channels for sensorA, sensorB, and sensorC
import "math"

LIMIT = 0.1   // Set acceptable variation limit between sensors to 10%

type SafeTemp struct {
    Temp float64
    isSafe bool
}

// variation returns relative error between inputs. Unfortunately, "error" was taken
func variation(a, b float64) float64 {
    return math.Abs((a - b) / (a + b))
}

// safify zips together temperatures so long as error is below LIMIT
func safify(chA, chB, chC <-chan float64, chOut chan<- SafeTemp) {
    for {
        a, aOk := <-chA
        b, bOk := <-chB
        c, cOk := <-chC

        if !(aOk && bOk && cOk) {
            close(chOut)
            return
        }

        if variation(a, b) < LIMIT && variation(b, c) < LIMIT &&
                variation(c, a) < LIMIT {
            chOut <- SafeTemp{ (a + b + c) / 3, true }
        } else {
            chOut <- SafeTemp{ 0.0, false }
        }

    }
}

Теперь количество вещей, которые мне нужно проверить для функции конвейера ( safify), значительно увеличивается:

  • Ставит значение на выходной канал, когда вы получаете его на всех входных каналах
  • Не устанавливает значение для выходного канала, если вы не получаете его на всех входных каналах
  • Время истекает, если выходной канал занимает слишком много времени после входов на всех трех входных каналах, но только на всех трех
  • Закрывает выходной канал при закрытии любого входного канала
  • Не закрывает выходной канал, если ни один входной канал не закрыт
  • Флажки так, как isSafeбудто первый канал значительно отличается от других, с таймаутами
  • Флажки как нет, isSafeесли второй канал значительно отличается от других, с таймаутами
  • Пометить как нет, isSafeесли третий канал значительно отличается от других, с таймаутами
  • Пометить как нет, isSafeесли все каналы существенно отличаются от других, с таймаутами

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

Похоже, что многие из этих проверок (за исключением тех, которые связаны с правильными вычислениями) являются общими практически для любой функции конвейера канала в стиле fan-in-style в Go, и проблема остановки гарантирует, что мы должны использовать таймауты для всех этих операций, если только мы не хотим, чтобы остановка наших тестов зависела от остановки и возможного проталкивания канала тестируемых функций.

Учитывая, насколько похожи эти типы тестов по всем направлениям, и как я в конечном итоге пишу довольно похожие тесты, по сути, проверяющие, насколько хорошо эти функции конвейера канала соответствуют гарантиям основных функций конвейера канала , а не поведения функций , снова и снова , есть ли:

  1. Стандартный набор практик для таких тестов надежности работы конвейера ИЛИ
  2. Стандартный или хорошо защищенный фреймворк или набор фреймворков для тестирования функций канала?

Ответы

2 KarlBielefeldt Aug 16 2020 at 18:27

Вы смешиваете две разные проблемы. Если вы сделали отдельную абстракцию для конвейера, вы можете протестировать ее один раз. Что-то вроде (простите синтаксис, я не знаю):

func double(v int) int {
    return v * 2
}

pipeline(in, out, double)

или же

func safe(v [3]float64) SafeTemp {
    if variation(v[0], v[1]) < LIMIT && variation(v[1], v[2]) < LIMIT &&
            variation(v[2], v[0]) < LIMIT {
        return SafeTemp{ (v[0] + v[1] + v[2]) / 3, true }
    } else {
        return SafeTemp{ 0.0, false }
    }
}

pipeline(in, out, safe)

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