Engineering

Testen Sie Ihre Datenstrukturen mit go-lookslike

Ich möchte Ihnen eine neue Open-Source-Bibliothek mit Test- und Schemavalidierungsfunktionen für Go vorstellen, die wir hier bei Elastic entwickelt haben. Sie heißt Lookslike. Mit Lookslike können Sie Vergleiche mit der Form Ihrer golang-Datenstrukturen anstellen, ähnlich wie mit einem JSON-Schema, jedoch mit mehr Funktionen und enger an den Go-Stil angelehnt. Diese Bibliothek enthält eine Reihe von Funktionen, die wir in keiner vorhandenen Go-Testbibliothek gefunden haben.

Lassen Sie uns mit einem Beispiel für ihren Leistungsumfang beginnen:

// Mit dieser Bibliothek können wir überprüfen, ob eine Datenstruktur einem bestimmten Schema exakt entspricht oder ähnelt.
// Mit dem folgenden Code können wir testen, ob ein Haustier ein Hund oder eine Katze ist.

// Ein Hund namens Rover
rover := map[string]interface{}{"name": "rover", "barks": "often", "fur_length": "long"}
// Eine Katze namens Pounce
pounce := map[string]interface{}{"name": "pounce", "meows": "often", "fur_length": "short"}

// Wir definieren einen Validator
// Wir definieren einen Hund als Objekt mit den folgenden Eigenschaften:
// 1. Ein Schlüssel „name“ als nicht leere Zeichenfolge mit der integrierten Definition „IsNonEmptyString“
// 2. Ein Schlüssel „fur_length“ (Felllänge) mit den möglichen Werten „long“ oder „short“
// 3. Ein Schlüssel „barks“ (bellt) mit den möglichen Werten „often“ (oft) oder „rarely“ (selten)
// 4. Außerdem definieren wir einen strikte Übereinstimmung, um alle Schlüssel außer
//    den genannten Werten als Fehler zu behandeln
dogValidator := lookslike.Strict(lookslike.MustCompile(map[string]interface{}{
	"name":       isdef.IsNonEmptyString,
	"fur_length": isdef.IsAny(isdef.IsEqual("long"), isdef.IsEqual("short")),
	"barks": isdef.IsAny(isdef.IsEqual("often"), isdef.IsEqual("rarely")),
}))

result := dogValidator(rover)
fmt.Printf("Checked rover, validation status %t, errors: %v\n", result.Valid, result.Errors())
result = dogValidator(pounce)
fmt.Printf("Checked pounce, validation status %t, errors: %v\n", result.Valid, result.Errors())

Wenn wir diesen Code ausführen, erhalten wir die unten gezeigte Ausgabe.

Checked rover, validation status true, errors: []
Checked pounce, validation status false, errors: [@Path 'barks': expected this key to be present @Path 'meows': unexpected field encountered during strict validation]

Wie sie sehen, ergab der Hund „rover“ wie erwartet einen Treffer, die Katze „pounce“ dagegen nicht, was zu zwei Fehlern führt. Ein Fehler, weil kein Schlüssel „barks“ definiert war, und der andere Fehler, weil der zusätzliche unerwartete Schlüssel „meows“ (miaut) vorhanden war.

Da Lookslike normalerweise für Tests eingesetzt wird, können wir den Helfer testslike.Test verwenden, um unsere Testausgabe zu formatieren. Ändern Sie die letzten Zeilen im obigen Beispiel wie folgt.

testslike.Test(t, dogValidator, rover)
testslike.Test(t, dogValidator, pounce)

Kombinationen

Die Möglichkeit, Validatoren zu kombinieren, ist eines der Kernkonzepte in Lookslike. Angenommen, wir möchten separate Validatoren für Katzen und Hunde, aber wir möchten allgemeine Felder wie „name“ und „fur_length“ nicht jeweils neu definieren. Sehen wir uns dazu das folgende Beispiel näher an.

pets := []map[string]interface{}{
	{"name": "rover", "barks": "often", "fur_length": "long"},
	{"name": "lucky", "barks": "rarely", "fur_length": "short"},
	{"name": "pounce", "meows": "often", "fur_length": "short"},
	{"name": "peanut", "meows": "rarely", "fur_length": "long"},
}

// Wir sehen, dass alle Haustiere die Eigenschaft „fur_length“ haben, aber nur Katzen miauen, und Hunde bellen.
// Mit „lookslike.Compose“ können wir diese Information präzise kodieren.
// Außerdem sehen wir, dass „meows“ und „barks“ dieselbe Enumeration von Werten enthalten.
// Wir erstellen zunächst ein zusammengesetztes „IsDef“ mit der „IsAny“-Komposition, um ein neues „IsDef“ als
// ein logisches „und“ seiner „IsDef“-Argumente zu erstellen

isFrequency := isdef.IsAny(isdef.IsEqual("often"), isdef.IsEqual("rarely"))

petValidator := MustCompile(map[string]interface{}{
	"name":       isdef.IsNonEmptyString,
	"fur_length": isdef.IsAny(isdef.IsEqual("long"), isdef.IsEqual("short")),
})
dogValidator := Compose(
	petValidator,
	MustCompile(map[string]interface{}{"barks": isFrequency}),
)
catValidator := Compose(
	petValidator,
	MustCompile(map[string]interface{}{"meows": isFrequency}),
)

for _, pet := range pets {
	var petType string
	if dogValidator(pet).Valid {
		petType = "dog"
	} else if catValidator(pet).Valid {
		petType = "cat"
	}
	fmt.Printf("%s is a %s\n", pet["name"], petType)
}

// Ausgabe:
// rover is a dog
// lucky is a dog
// pounce is a cat
// peanut is a cat

Warum wir Lookslike entwickelt haben

Lookslike ist aus dem Heartbeat-Projekt bei Elastic entstanden. Heartbeat ist der Agent für unsere Uptime-Lösung und sendet Pings an die Endpunkte, um deren Erreichbarkeit zu überprüfen. Heartbeat generiert letztendlich Elasticsearch-Dokumente, die in unserer golang-Codebasis als „map[string]interface{}“ abgebildet werden. Lookslike wurde ursprünglich entwickelt, um diese Ausgabedokumente zu testen, aber inzwischen wird die Bibliothek auch an anderen Stellen in der Beats-Codebasis eingesetzt.

Herausforderungen bei der Entwicklung:

  • Die Daten in manchen Feldern müssen unpräzise abgeglichen werden, zum Beispiel die Ausführungsdauer in „monitor.duration“. Diese Werte können zwischen einzelnen Ausführungen schwanken. Außerdem wollten wir Daten lose abgleichen können.
  • Bei sämtlichen Tests wurden zahlreiche Felder mit anderen Tests gemeinsam verwendet, und nur wenige Felder enthielten Abweichungen. Wir wollten doppelt vorhandenen Code durch unterschiedliche Felddefinitionen minimieren.
  • Wir wollten optimierte Testausgaben mit separaten Fehlern für einzelne Probleme in Feldern, daher der Testhelfer „testslike“.

Angesichts dieser Herausforderungen haben wir uns für das folgende Design entschieden:

  • Wir benötigten ein flexibles Schema, mit dem Entwickler neue Abgleiche mühelos erstellen können. Alle Schemas sollten kombinierbar und verschachtelbar sein, um Dokumente ineinander verschachteln zu können, indem man Schemas kombiniert, ohne dafür Code duplizieren zu müssen.
  • Wir brauchten einen guten Testhelfer, um Testfehler gut lesbar zu präsentieren.

Wichtige Typen

Die Architektur von „Lookslike“ basiert auf zwei wichtigen Typen: „Validator“ und „IsDef“. Ein Validator ist das Ergebnis der Kompilierung eines Schemas. Diese Funktion nimmt eine beliebige Datenstruktur entgegen und gibt ein Ergebnis zurück. „IsDef“ wird verwendet, um einzelne Felder abzugleichen. Vielleicht fragen Sie sich jetzt, warum wir diese beiden Objekte unterscheiden. Und in der Tat werden wir sie irgendwann möglicherweise zusammenführen. Der Hauptgrund dafür ist jedoch, dass „IsDef“ zusätzliche Argumente zu seinem Ort in der Dokumentstruktur erhält, um mit diesem Kontext zusätzliche Überprüfungen durchführen zu können. Validator-Funktionen erhalten keinen zusätzlichen Kontext, sind dafür jedoch einfacher auszuführen (sie nehmen einfach „interface{}“ entgegen und validieren die Eingabe).

In den Quellcode-Dateien finden Sie verschiedene Beispiele für benutzerdefinierte „IsDefs“. Sie können Ihren eigenen Code mit neuen „IsDefs“ erweitern.

Praktische Beispiele

Wir setzen Lookslike in Beats an vielen Stellen ein. Mit dieser github-Suche finden Sie eine Vielzahl von Anwendungsbeispielen.

Wir brauchen Ihre Hilfe!

Falls Sie sich für Lookslike interessieren, stellen Sie eine pull-Anfrage im Repository! Wir könnten insbesondere einen umfassenderen Satz an „IsDefs“ gebrauchen.

Weitere Informationen

Wir haben uns größte Mühe gegeben, um Lookslike ansprechend zu gestalten. Sie finden die Dokumente zu Lookslike auf godoc.org.