Как тестировать конвейеры каналов в Go
Я довольно часто использую в 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, и проблема остановки гарантирует, что мы должны использовать таймауты для всех этих операций, если только мы не хотим, чтобы остановка наших тестов зависела от остановки и возможного проталкивания канала тестируемых функций.
Учитывая, насколько похожи эти типы тестов по всем направлениям, и как я в конечном итоге пишу довольно похожие тесты, по сути, проверяющие, насколько хорошо эти функции конвейера канала соответствуют гарантиям основных функций конвейера канала , а не поведения функций , снова и снова , есть ли:
- Стандартный набор практик для таких тестов надежности работы конвейера ИЛИ
- Стандартный или хорошо защищенный фреймворк или набор фреймворков для тестирования функций канала?
Ответы
Вы смешиваете две разные проблемы. Если вы сделали отдельную абстракцию для конвейера, вы можете протестировать ее один раз. Что-то вроде (простите синтаксис, я не знаю):
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
абстракцию, поэтому вам придется принять определенное количество дублирования. Однако вы должны иметь возможность хотя бы отделить проблемы, связанные с шаблоном конвейера, от логики, более специфичной для приложения.