Jak testować potoki kanału w Go
Często używam wzorca „channel pipeline” w Go, który wygląda mniej więcej tak:
// 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)
Problem, na który wielokrotnie napotykam, polega na tym, że muszę przetestować wiele różnych funkcji tych funkcji potokowych:
- Umieszcza wartość na kanale wyjściowym po umieszczeniu jej na kanale wejściowym
- Nie umieszcza wartości na kanale wyjściowym, jeśli nie umieścisz jej na kanale wejściowym
- Przekracza limit czasu, jeśli kanał wyjściowy trwa zbyt długo po pojawieniu się wartości na kanale wejściowym
- Zamyka kanał wyjściowy po zamknięciu kanału wejściowego
- Nie zamyka kanału wyjściowego przed zamknięciem kanału wejściowego
- Wykonuje poprawną transformację na wejściu
Co więcej, to tylko naprawdę prosty przykład. Bardziej typowe przypadki wyglądają mniej więcej tak, jak w tym przykładzie, w którym próbujemy użyć nadmiarowych czujników temperatury, aby znaleźć błędy w danych wyjściowych:
// 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 }
}
}
}
Teraz liczba rzeczy, które muszę przetestować dla funkcji potoku ( safify
) znacznie wzrasta:
- Ustawia wartość na kanale wyjściowym, gdy uzyskasz ją na wszystkich kanałach wejściowych
- Nie umieszcza wartości na kanale wyjściowym, jeśli nie masz jej na wszystkich kanałach wejściowych
- Przekracza limit czasu, jeśli kanał wyjściowy trwa zbyt długo po wejściu na wszystkie trzy kanały wejściowe, ale tylko we wszystkich trzech
- Zamyka kanał wyjściowy po zamknięciu dowolnego kanału wejściowego
- Nie zamyka kanału wyjściowego, jeśli żaden kanał wejściowy nie jest zamknięty
- Flagi jakby nie,
isSafe
jeśli pierwszy kanał różni się znacznie od innych, z limitami czasu - Flagi,
isSafe
jakby drugi kanał znacznie się różnił od innych, z limitami czasu - Flagi,
isSafe
jakby trzeci kanał znacznie się różnił od innych, z limitami czasu - Flagi jak nie
isSafe
, czy wszystkie kanały różnią się znacznie od innych, ze limity czasu
Co więcej, trzy kanały wejściowe mogą nie być ze sobą zsynchronizowane, co jeszcze bardziej zwiększa złożoność poza przedstawioną powyżej.
Wygląda na to, że wiele z tych sprawdzeń (z wyjątkiem tych, które mają do czynienia z poprawnymi obliczeniami) jest wspólnych dla każdej funkcji potoku kanału w stylu fan-in w Go, a Problem z zatrzymaniem gwarantuje, że musimy użyć limitów czasu dla wszystkich tych operacji, chyba że chcemy, aby zatrzymanie naszych testów zależało od zatrzymywania i ewentualnego wypychania kanału przez testowane funkcje.
Biorąc pod uwagę, jak podobne są te typy testów we wszystkich obszarach i jak w końcu piszę dość podobne testy, w zasadzie testując, jak dobrze te funkcje potoku kanału są zgodne z podstawowymi gwarancjami funkcji potoku kanału , zamiast zachowania funkcji , w kółko , czy istnieje:
- Standardowy zestaw praktyk dotyczących tego rodzaju testów niezawodności działania potoków kanałowych LUB
- Standardowa lub dobrze zabezpieczona struktura lub zestaw struktur do testowania funkcji natywnych dla kanału?
Odpowiedzi
Mieszasz dwie różne kwestie. Jeśli utworzyłeś osobną abstrakcję dla potoku, możesz to raz przetestować. Coś takiego (wybacz składnię, nie wiem, idź):
func double(v int) int {
return v * 2
}
pipeline(in, out, double)
lub
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)
Bez polimorfizmu parametrycznego nie można tak naprawdę stworzyć w pełni ogólnej pipeline
abstrakcji, więc musisz zaakceptować pewien stopień powielenia. Należy jednak być w stanie przynajmniej oddzielić obawy dotyczące wzorca potoku od logiki bardziej specyficznej dla aplikacji.