So testen Sie Kanalpipelines in Go

Aug 15 2020

Ich benutze das "Channel-Pipeline" -Muster in Go ziemlich oft, was ungefähr so ​​aussieht:

// 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)

Das Problem, auf das ich wiederholt stoße, ist, dass ich verschiedene Funktionen dieser Pipeline-Funktionen testen muss:

  • Setzt einen Wert auf den Ausgangskanal, wenn Sie einen auf den Eingangskanal legen
  • Setzt keinen Wert auf den Ausgangskanal, wenn Sie keinen auf den Eingangskanal legen
  • Zeitüberschreitung, wenn der Ausgangskanal zu lange dauert, nachdem ein Wert auf dem Eingangskanal angezeigt wurde
  • Schließt den Ausgangskanal, wenn der Eingangskanal geschlossen wird
  • Schließt den Ausgangskanal nicht, bevor der Eingangskanal geschlossen wird
  • Führt die korrekte Transformation für die Eingabe durch

Darüber hinaus ist dies nur ein wirklich einfaches Beispiel. Typischere Fälle sehen ungefähr so ​​aus wie in diesem Beispiel, wobei wir versuchen, redundante Temperatursensoren zu verwenden, um Fehler in der Ausgabe zu finden:

// 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 }
        }

    }
}

Jetzt safifysteigt die Anzahl der Dinge, die ich für die Pipeline-Funktion ( ) testen muss, erheblich an:

  • Setzt einen Wert auf den Ausgangskanal, wenn Sie einen auf allen Eingangskanälen erhalten
  • Setzt keinen Wert auf den Ausgangskanal, wenn Sie nicht auf allen Eingangskanälen einen erhalten
  • Zeitüberschreitung, wenn der Ausgangskanal nach den Eingängen auf allen drei Eingangskanälen zu lange dauert, jedoch nur auf allen drei
  • Schließt den Ausgangskanal, wenn ein Eingangskanal geschlossen wird
  • Schließt den Ausgangskanal nicht, wenn kein Eingangskanal geschlossen ist
  • Flags als nicht, isSafewenn der erste Kanal mit Zeitüberschreitungen erheblich von anderen abweicht
  • Flags als nicht, isSafewenn der zweite Kanal mit Zeitüberschreitungen erheblich von anderen abweicht
  • Flags als nicht, isSafewenn der dritte Kanal mit Zeitüberschreitungen erheblich von anderen abweicht
  • Flags als nicht, isSafewenn alle Kanäle mit Timeouts erheblich von anderen abweichen

Ferner können die drei Eingangskanäle nicht mehr miteinander synchronisiert sein, was zu einer erheblichen Komplexität führt, die noch über die oben gezeigte hinausgeht.

Es scheint, dass viele dieser Überprüfungen (mit Ausnahme derjenigen, die mit korrekten Berechnungen zu tun haben) im Grunde jeder Kanal-Pipeline-Funktion im Fan-Stil in Go gemeinsam sind, und das Halteproblem garantiert, dass wir Zeitüberschreitungen für alle verwenden müssen von diesen Operationen, es sei denn, wir möchten, dass das Anhalten unserer Tests vom Anhalten und eventuellen Channel-Pushing-Verhalten der getesteten Funktionen abhängt.

Bedenkt man , wie ähnlich diese Art von Tests auf der ganzen Linie sind, und wie ich am Ende das Schreiben ganz ähnliche Tests im Wesentlichen zu testen , wie gut diese Kanal Pipeline Funktionen entsprechen Basiskanal Pipeline - Funktion gewährleistet , statt Verhalten der Funktionen , immer und immer und immer wieder gibt es entweder:

  1. Ein Standardsatz von Praktiken rund um diese Art von Zuverlässigkeitstests für Kanalpipeline-Funktionen ODER
  2. Ein Standard- oder gut gehärtetes Framework oder eine Reihe von Frameworks zum Testen kanal-nativer Funktionen?

Antworten

2 KarlBielefeldt Aug 16 2020 at 18:27

Sie mischen zwei verschiedene Anliegen. Wenn Sie eine separate Abstraktion für die Pipeline erstellt haben, können Sie diese einmal testen. So etwas wie (Syntax verzeihen, ich weiß nicht gehen):

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

pipeline(in, out, double)

oder

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)

Ohne parametrischen Polymorphismus können Sie keine vollständig generische pipelineAbstraktion erstellen, daher müssen Sie eine gewisse Duplizierung akzeptieren. Sie sollten jedoch in der Lage sein, zumindest die Bedenken des Pipeline-Musters von der anwendungsspezifischeren Logik zu trennen.