Pruebas las formas de los datos con go-lookslike | Elastic Blog
Engineering

Pruebas las formas de los datos con go-lookslike

Nos complace presentarte una nueva biblioteca de validación de prueba/esquema open source de Go que hemos desarrollado aquí en Elastic. Se llama Lookslike. Lookslike te permite comparar la forma de tus estructuras de datos Golang de una manera similar a JSON Schema, pero más potente y con más estilo Go. Tiene ciertas funcionalidades que no pudimos encontrar en las librerías de prueba existentes de Go.

Veamos un ejemplo de su poder:

// Esta biblioteca nos permite revisar que una estructura de datos sea similar o exactamente igual a cierto esquema.
// Por ejemplo, podríamos probar si una mascota es un perro o un gato con el código que aparece a continuación.

// Un perro llamado Rover
rover := map[string]interface{}{"name": "rover", "barks": "often", "fur_length": "long"}
// Un gato llamado Pounce
pounce := map[string]interface{}{"name": "pounce", "meows": "often", "fur_length": "short"}

// Aquí definimos un validador
// Aquí definimos que un perro es un elemento que contiene lo siguiente:
// 1. Una clave "name" (nombre) que sea un texto no vacío, según la definición incorporada `IsNonEmptyString`.
// 2. Una clave "fur_length" (largo del pelo) que tenga "largo" o "corto" como valor.
// 3. Una clave "barks" (ladra) que tenga "a menudo" o "raramente" como valor.
// 4. También lo definimos como una comparación estricta, lo que significa que si existen otras claves además de esas
// deberán considerarse errores.
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())

Ejecutar el código anterior dará como resultado lo que aparece a continuación.

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]

Aquí podemos ver que "Rover", el perro, dio la coincidencia esperada, y que "Pounce", el gato no lo hizo y dio dos errores. Un error fue que no tenía una clave "barks" definida. El otro fue que contenía una clave extra, "meows" (maúlla), que no se anticipó.

Como Lookslike se suele usar en un contexto de prueba, contamos con el asistente testslike.Test, que produce resultados de prueba con un buen formato. Solo necesitas cambiar las últimas líneas del ejemplo anterior por lo que se muestra a continuación.

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

Composición

Ser capaz de combinar validadores es un concepto clave en Lookslike. Digamos que queremos separar los validadores gato y perro, pero no queremos redefinir campos comunes como name y fur_length en cada uno. Veamos eso en el ejemplo a continuación.

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"},
}

// Podemos ver que todas las mascotas cuentan con la propiedad "fur_length", pero que solo los gatos maúllan, y los perros ladran.
// Podemos codificar esto de forma concisa en lookslike usando lookslike.Compose.
// También podemos ver que tanto "meows" como "barks" contienen los mismos valores.
// Comenzaremos creando un IsDef compuesto con la composición IsAny, lo cual crea un nuevo IsDef que consiste en
// un 'or' lógico de sus argumentos IsDef.

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

// Salida:
// rover es un perro
// lucky es un perro
// pounce es un gato
// peanut es un gato

Por qué creamos Lookslike

Lookslike surgió del proyecto Heartbeat aquí en Elastic. Heartbeat es el agente detrás de la solución Uptime. Envía puntos finales y después crea reportes sobre si están activos o no. La salida final de Heartbeat son los documentos de Elasticsearch, representados como tipos map[string]interface{} en nuestra base de códigos de Golang. Probar estos documentos de salida fue lo que creó la necesidad de esta biblioteca, aunque hoy en día se use en otros lugares de la base de código Beats.

Los desafíos a los que nos enfrentamos son los siguientes:

  • Algunos campos contienen datos que debieron combinarse de forma imprecisa, como monitor.duration, que cronometra el tiempo que demoró una ejecución. Esto puede variar entre ejecuciones. Queríamos contar con una forma de combinar datos de forma más libre.
  • En cualquier prueba, muchos campos se compartieron con otras pruebas, con solo algunas variaciones. Queríamos contar con la posibilidad de reducir la duplicación de código componiendo diferentes definiciones de campo.
  • Queríamos una buena salida de prueba que muestre las fallas individuales de los campos como errores individuales. De ahí surge el asistente de prueba testslike.

Dados estos desafíos, tomamos las siguientes decisiones de diseño:

  • Queríamos que el esquema fuera flexible, y que fuera fácil para los desarrolladores crear nuevas combinaciones.
  • Queríamos que se puedan componer y anidar todos los esquemas, de modo tal que, en el caso de que anides un documento, puedas simplemente combinar esquemas sin duplicar una parte del código.
  • Necesitábamos un buen asistente de prueba para que las fallas de la prueba sean fáciles de leer.

Tipos de clave

La arquitectura de Lookslike gira en torno de dos tipos principales: Validator e IsDef. UnValidator (validador) es el resultado de la compilación de un esquema dado. Es una función que toma una estructura de datos arbitraria y devuelve un resultado. Un IsDef es el tipo usado para combinar un campo individual. Quizás te estés preguntando por qué distinguimos estos dos elementos. De hecho, puede que en el futuro los fusionemos. Sin embargo, la razón principal es que IsDef obtiene argumentos adicionales sobre su localización en la estructura del documento que le permite llevar a cabo validaciones avanzadas con base en ese contexto. Las funciones de Validator no reciben contenido adicional, pero son más amigables en cuanto a la ejecución (solo toman interface{} y validan eso).

Para ver ejemplos de escritura personalizada de IsDefs, observa los archivos fuente. Puedes agregar nuevos IsDef en tu propia fuente para extenderlo.

Ejemplos en el campo

Usamos Lookslike ampliamente en Beats. Puedes encontrar muchos ejemplos de su uso con esta búsqueda en github.

¡Necesitamos tu ayuda!

Si te interesa Lookslike, envía una solicitud de pull en el repositorio. Sería útil contar con un conjunto de IsDef en particular más amplio.

Aprender más

Hemos hecho un gran esfuerzo para documentar Lookslike de la mejor manera. Puedes buscar los documentos de Lookslike en godoc.org.