Engineering

Teste de amostras de dados com o Lookslike

Gostaria de apresentar uma nova biblioteca open source de validação de esquemas/testes Go que desenvolvemos na Elastic. Ela se chama Lookslike. O Lookslike permite fazer a correspondência com a forma de estruturas de dados de golang de maneira semelhante ao JSON Schema, mas com mais eficácia e semelhança com o Go. Ele oferece inúmeros recursos que não conseguimos encontrar em nenhuma biblioteca de testes Go existente.

Vamos tratar diretamente de um exemplo desses recursos:

// Esta biblioteca permite verificar se uma estrutura de dados é semelhante ou exatamente igual a um determinado esquema.
// Por exemplo, poderíamos testar se um animal de estimação é um cachorro ou um gato, usando o código a seguir.

// Um cachorro chamado rover
rover := map[string]interface{}{"name": "rover", "barks": "often", "fur_length": "long"}
// Um gato chamado pounce
pounce := map[string]interface{}{"name": "pounce", "meows": "often", "fur_length": "short"}

// Aqui definimos um validador
// Aqui definimos um cachorro como item que tem:
// 1. uma chave "name" que é qualquer sequência de caracteres não vazia, usando a definição interna `IsNonEmptyString`
// 2. uma chave "fur_length" que tem valor "long" ou "short"
// 3. uma chave "bars" que tem valor "often" ou "rarely"
// 4. ainda definimos isto como um elemento de correspondência estrito, significando que se alguma chave diferente dessas
//    listadas estiver presente, deverá ser considerada erro
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())

A execução do código anterior imprimirá o que é exibido a seguir.

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]

Aqui podemos ver que "rover", o cachorro, teve correspondência conforme esperado, e que "pounce", o gato, não teve, o que gerou dois erros. Um dos erros foi que não houve chave barks definida; o outro foi que houve uma chave extra, meows, que não foi prevista.

Como o Lookslike normalmente é usado em um um contexto de testes, temos o auxiliar testslike.Test que produz saída de teste bem formatada. Você alteraria as últimas linhas do exemplo anterior para o que é mostrado a seguir.

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

Composição

Ser capaz de combinar validadores é um conceito crucial no Lookslike. Vamos supor que quiséssemos validadores de gato e cachorro separados, mas não quiséssemos redefinir campos comuns como name e fur_length em cada um deles. Vamos analisar isso no exemplo a seguir.

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 todos os animais de estimação têm a propriedade "fur_length", mas que somente gatos miam (meow) e cachorros latem (bark).
// Podemos codificar concisamente isso no Lookslike usando lookslike.Compose.
// Também podemos ver que tanto "meows" quanto "barks" contém as mesmas enumerações de valores.
// Começaremos criando um IsDef composto usando a composição IsAny, que cria um novo IsDef que é
// um 'or' lógico dos 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)
}

// Saída:
// rover é um cachorro
// lucky é um cachorro
// pounce é um gato
// peanut é um gato

Por que desenvolvemos o Lookslike

O Lookslike é oriundo do projeto Heartbeat aqui na Elastic. O Heartbeat é o agente por trás da nossa solução Uptime; ele faz ping de endpoints e relata se eles estão ativos ou inativos. A saída excelente do Heartbeat são os documentos do Elasticsearch, representados como tipos de map[string]interface{} em nossa base de código golang. O teste desses documentos de saída foi o que criou a necessidade dessa biblioteca, apesar de agora ela ser usada em qualquer parte na base de código do Beats.

Os desafios que enfrentamos foram estes:

  • Alguns campos tinham dados que precisavam ter correspondência imprecisa, como monitor.duration que marcava o tempo de duração da execução. Isso poderia variar entre as execuções. Queríamos uma maneira de fazer uma correspondência flexível dos dados.
  • Em qualquer teste considerado, muitos campos foram compartilhados com outros testes, com apenas algumas variações. Queríamos poder reduzir a duplicidade do código compondo diferentes definições de campo.
  • Queríamos uma boa saída de testes mostrando falhas de campo individuais como erros individuais, consequentemente um auxiliar de teste testslike.

Considerando esses desafios, tomamos as seguintes decisões de design:

  • Queríamos que o esquema fosse flexível e que fosse simples para os desenvolvedores criarem novos elementos de correspondência facilmente.
  • Queríamos que todos os esquemas pudessem ser compostos e aninhados, de maneira que, se você aninhasse um documento em outro, poderia apenas combinar esquemas sem duplicar uma linha sequer de código.
  • Precisávamos de um bom auxiliar de teste para tornar as falhas de teste legíveis.

Tipos de chave

A arquitetura do Lookslike consiste em dois tipos principais: Validator e IsDef. Um Validator é o resultado de compilar um determinado esquema. É uma função que pega uma estrutura de dados arbitrária e retorna um resultado. Um IsDef é o tipo usado para fazer correspondência de um campo individual. Talvez você imagine por que há uma distinção entre eles. Na realidade, podemos mesclar esses tipos no futuro. Entretanto, o principal motivo é que um IsDef obtém argumentos adicionais sobre sua localização na estrutura de documentos que lhe permite executar validações avançadas com base nesse contexto. As funções do Validator não recebem contexto adicional, mas são mais amigáveis de executar (elas apenas pegam interface{} e validam isso).

Para obter exemplos de como escrever IsDefs personalizados, basta analisar os arquivos fonte. Você pode adicionar novos IsDefs à sua própria fonte para estendê-la.

Exemplos no campo

Usamos o Lookslike extensivamente no Beats. Você pode encontrar inúmeros exemplos de seu uso com esta busca no github.

Precisamos de sua ajuda!

Se tiver interesse no Lookslike, envie uma solicitação pull no repositório! Poderíamos usar um conjunto mais abrangente de IsDefs em particular.

Aprendendo mais

Fizemos um enorme esforço para documentar o Lookslike também. Você pode procurar os documentos do Lookslike em godoc.org.