Come testare le pipeline di canale in Go

Aug 15 2020

Uso spesso il pattern "channel pipeline" in Go, che assomiglia a questo:

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

Il problema in cui mi sono imbattuto ripetutamente è che devo testare una serie di caratteristiche diverse di queste funzioni della pipeline:

  • Mette un valore sul canale di uscita quando ne metti uno sul canale di ingresso
  • Non mette un valore sul canale di uscita quando non ne metti uno sul canale di ingresso
  • Timeout se il canale di uscita impiega troppo tempo dopo che un valore appare sul canale di ingresso
  • Chiude il canale di uscita quando si chiude il canale di ingresso
  • Non chiude il canale di uscita prima che il canale di ingresso sia chiuso
  • Esegue la trasformazione corretta sull'input

Inoltre, questo è solo un esempio davvero semplice. I casi più tipici assomigliano a questo esempio, in cui stiamo cercando di utilizzare sensori di temperatura ridondanti per trovare errori nell'output:

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

    }
}

Ora il numero di cose che devo testare per la funzione pipeline ( safify) aumenta in modo significativo:

  • Mette un valore sul canale di uscita quando ne ottieni uno su tutti i canali di ingresso
  • Non mette un valore sul canale di uscita quando non ne ottieni uno su tutti i canali di ingresso
  • Timeout se il canale di uscita impiega troppo tempo dopo gli ingressi su tutti e tre i canali di ingresso, ma solo su tutti e tre
  • Chiude il canale di uscita quando nessun chiude canali di ingresso
  • Non chiude il canale di uscita se nessun canale di ingresso è chiuso
  • Contrassegna come no isSafese il primo canale varia in modo significativo dagli altri, con timeout
  • Contrassegna come no isSafese il secondo canale varia in modo significativo dagli altri, con timeout
  • Contrassegna come no isSafese il terzo canale varia in modo significativo dagli altri, con timeout
  • Contrassegna come no isSafese tutti i canali variano in modo significativo dagli altri, con timeout

Inoltre, i tre canali di ingresso possono non essere sincronizzati tra loro, il che aggiunge una complessità significativa ancora oltre quella mostrata sopra.

Sembra che molti di questi controlli (tranne in particolare quelli che hanno a che fare con calcoli corretti) siano comuni praticamente a qualsiasi funzione di pipeline di canale in stile fan in Go, e il problema di arresto garantisce che dobbiamo usare i timeout per tutti di queste operazioni, a meno che non vogliamo che l'arresto dei nostri test dipenda dall'arresto e dall'eventuale comportamento di channel-push delle funzioni testate.

Dato quanto sono simili questi tipi di test su tutta la linea, e come finisco per scrivere test abbastanza simili essenzialmente testando quanto bene queste funzioni della pipeline di canale siano conformi alle garanzie di base della pipeline di canale , invece del comportamento delle funzioni , ancora e ancora e ancora , c'è o:

  1. Un insieme standard di pratiche relative a questi tipi di test di affidabilità della funzione della pipeline di canale OPPURE
  2. Un framework standard o ben consolidato o un set di framework per testare le funzioni native del canale?

Risposte

2 KarlBielefeldt Aug 16 2020 at 18:27

Stai mescolando due diverse preoccupazioni. Se hai creato un'astrazione separata per la pipeline, potresti testarla una volta. Qualcosa del tipo (perdona la sintassi, non so andare):

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

pipeline(in, out, double)

o

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)

Senza polimorfismo parametrico, non puoi davvero creare pipelineun'astrazione completamente generica , quindi devi accettare una certa quantità di duplicazioni. Tuttavia, dovresti essere in grado di separare almeno le preoccupazioni del modello di pipeline dalla logica più specifica dell'applicazione.