Advertisement
  1. Code
  2. Go

Prueba de código intensivo de datos con Go, parte 5

Scroll to top
Read Time: 12 min
This post is part of a series called Testing Data-Intensive Code with Go.
Testing Data-Intensive Code With Go, Part 4

Spanish (Español) translation by Juan Pablo Diaz Cuartas (you can also view the original English article)

Visión de conjunto

Esta es la parte cinco de cinco en una serie de tutoriales sobre la prueba de código de datos intensivos. En la parte cuatro, cubrí los almacenes de datos remotos, usando bases de datos de prueba compartidas, usando instantáneas de datos de producción y generando tus propios datos de prueba. En este tutorial, revisaré las pruebas de fuzz, probaré tu caché, probaré la integridad de los datos, probaré la idempotencia y los datos faltantes.

Prueba de Fuzz

La idea de las pruebas de fuzz es abrumar al sistema con muchas entradas aleatorias. En lugar de tratar de pensar en una información que cubra todos los casos, que puede ser difícil y / o muy laboriosa, dejas que la oportunidad lo haga por ti. Es conceptualmente similar a la generación de datos aleatorios, pero la intención aquí es generar entradas aleatorias o semialeatorias en lugar de datos persistentes.

¿Cuándo es útil la prueba de Fuzz?

La prueba de Fuzz es útil en particular para encontrar problemas de seguridad y rendimiento cuando las entradas inesperadas causan fallas o pérdidas de memoria. Pero también puede ayudar a garantizar que todas las entradas no válidas se detecten anticipadamente y el sistema las rechace adecuadamente.

Considere, por ejemplo, la entrada que viene en forma de documentos JSON profundamente anidados (muy comunes en las API web). Intentar generar manualmente una lista completa de casos de prueba es propenso a errores y mucho trabajo. Pero la prueba de fuzz es la técnica perfecta.

Usando Fuzz Testing

Hay varias bibliotecas que puede usar para realizar pruebas de fuzz. Mi favorito es gofuzz ​​de Google. Aquí hay un ejemplo simple que genera automáticamente 200 objetos únicos de una estructura con varios campos, incluida una estructura anidada.

1
import (
2
    "fmt"
3
  "github.com/google/gofuzz"
4
)
5
6
func SimpleFuzzing() {
7
	type SomeType struct {
8
		A string
9
		B string
10
		C int
11
		D struct {
12
			E float64
13
		}
14
	}
15
16
	f := fuzz.New()
17
	uniqueObject := SomeType{}
18
19
	uniqueObjects := map[SomeType]int{}
20
21
	for i := 0; i < 200; i++ {
22
		f.Fuzz(&object)
23
		uniqueObjects[object]++
24
	}
25
	fmt.Printf("Got %v unique objects.\n", len(uniqueObjects))
26
	// Output:

27
	// Got 200 unique objects.

28
}

Probando tu caché

Casi todos los sistemas complejos que manejan una gran cantidad de datos tienen un caché, o más probablemente varios niveles de cachés jerárquicos. Como dice el refrán, solo hay dos cosas difíciles en la ciencia de la computación: nombrar cosas, invalidar la caché y desactivarlo por un error.

Dejando de un lado las bromas, administrar su estrategia e implementación de almacenamiento en caché puede complicar el acceso a sus datos pero tener un tremendo impacto en el costo y el rendimiento de su acceso a los datos. La prueba de su caché no se puede realizar desde el exterior porque su interfaz oculta de dónde provienen los datos, y el mecanismo de caché es un detalle de implementación.

Veamos cómo probar el comportamiento de caché de la capa de datos híbridos de Songify.

Caché Hits and Misses

Los caches viven y mueren por su desempeño al azar. La funcionalidad básica de un caché es que, si los datos solicitados están disponibles en el caché (un acierto), se extraerán del caché y no del almacén de datos primario. En el diseño original de HybridDataLayer, el acceso a la memoria caché se realizó a través de métodos privados.

Las reglas de visibilidad go hacen imposible llamarlas directamente o reemplazarlas de otro paquete. Para habilitar las pruebas de caché, cambiaré esos métodos a funciones públicas. Esto está bien porque el código de aplicación real opera a través de la interfaz DataLayer, que no expone esos métodos.

El código de prueba, sin embargo, podrá reemplazar estas funciones públicas según sea necesario. Primero, agreguemos un método para obtener acceso al cliente de Redis, para que podamos manipular el caché:

1
func (m *HybridDataLayer) GetRedis() *redis.Client {
2
    return m.redis
3
}

A continuación, cambiaré los métodos getSongByUser_DB () a una variable de función pública. Ahora, en la prueba, puedo reemplazar la variable GetSongsByUser_DB () con una función que realiza un seguimiento de cuántas veces fue llamada y luego la reenvía a la función original. Eso nos permite verificar si una llamada a GetSongsByUser () buscó las canciones desde el caché o desde el DB.

Vamos a descomponerlo pieza por pieza. Primero, obtenemos la capa de datos (que también borra el DB y el redis), creamos un usuario y agregamos una canción. El método AddSong () también rellena redis.

1
func TestGetSongsByUser_Cache(t *testing.T) {
2
    now := time.Now()
3
	u := User{Name: "Gigi", 
4
             Email: "gg@gg.com", 
5
             RegisteredAt: now, LastLogin: now}
6
	dl, err := getDataLayer()
7
	if err != nil {
8
		t.Error("Failed to create hybrid data layer")
9
	}
10
11
	err = dl.CreateUser(u)
12
	if err != nil {
13
		t.Error("Failed to create user")
14
	}
15
16
	lm, err := NewSongManager(u, dl)
17
	if err != nil {
18
		t.Error("NewSongManager() returned 'nil'")
19
	}
20
21
	err = lm.AddSong(testSong, nil)
22
	if err != nil {
23
		t.Error("AddSong() failed")
24
	}

Esta es la parte buena Mantengo la función original y defino una nueva función instrumentada que incrementa la variable callCount local (está todo en un cierre) y llama a la función original. Luego, asigno la función instrumentada a la variable GetSongsByUser_DB. A partir de ahora, cada llamada de la capa de datos híbrida a GetSongsByUser_DB () irá a la función instrumentada.

1
    callCount := 0
2
	originalFunc := GetSongsByUser_DB
3
	instrumentedFunc := func(m *HybridDataLayer, 
4
	                         email string, 
5
                             songs *[]Song) (err error) {
6
		callCount += 1
7
		return originalFunc(m, email, songs)
8
	}
9
10
	GetSongsByUser_DB = instrumentedFunc

En este punto, estamos listos para realmente probar la operación de caché. Primero, la prueba llama al GetSongsByUser () del SongManager que lo reenvía a la capa de datos híbrida. Se supone que el caché está lleno para este usuario que acabamos de agregar. Entonces, el resultado esperado es que no se llamará a nuestra función instrumentada, y callCount permanecerá en cero.

1
    _, err = lm.GetSongsByUser(u)
2
	if err != nil {
3
		t.Error("GetSongsByUser() failed")
4
	}
5
6
	// Verify the DB wasn't accessed because cache should be

7
    // populated by AddSong()

8
	if callCount > 0 {
9
		t.Error(`GetSongsByUser_DB() called when it
10
                 shouldn't have`)
11
	}

El último caso de prueba es garantizar que si los datos del usuario no están en la memoria caché, se recuperarán correctamente de la base de datos. La prueba lo logra al enjuagar Redis (borrando todos sus datos) y hacer otra llamada a GetSongsByUser (). Esta vez, se llamará a la función instrumentada y la prueba verificará que callCount sea igual a 1. Finalmente, se restaurará la función GetSongsByUser_DB () original.

1
    // Clear the cache

2
	dl.GetRedis().FlushDB()
3
4
	// Get the songs again, now it's should go to the DB

5
    // because the cache is empty

6
	_, err = lm.GetSongsByUser(u)
7
	if err != nil {
8
		t.Error("GetSongsByUser() failed")
9
	}
10
11
	// Verify the DB was accessed because the cache is empty

12
	if callCount != 1 {
13
		t.Error(`GetSongsByUser_DB() wasn't called once 
14
                 as it should have`)
15
	}
16
17
	GetSongsByUser_DB = originalFunc
18
}

Invalidación de caché

Nuestro caché es muy básico y no invalida. Esto funciona bastante bien siempre que todas las canciones se agreguen a través del método AddSong () que se encarga de actualizar Redis. Si agregamos más operaciones, como eliminar canciones o eliminar usuarios, estas operaciones deberían actualizar Redis en consecuencia.

Este caché muy simple funcionará incluso si tenemos un sistema distribuido en el que múltiples máquinas independientes pueden ejecutar nuestro servicio Songify, siempre y cuando todas las instancias funcionen con las mismas instancias de DB y Redis.

Sin embargo, si la base de datos y la memoria caché se pueden desincronizar debido a operaciones de mantenimiento u otras herramientas y aplicaciones que cambian nuestros datos, entonces debemos elaborar una política de invalidación y actualización para la memoria caché. Se puede probar usando las mismas técnicas: reemplace las funciones objetivo o acceda directamente al DB y Redis en su prueba para verificar el estado.

Cachés LRU

Por lo general, no puedes dejar que el caché crezca infinitamente. Un esquema común para mantener los datos más útiles en la memoria caché es el caché LRU (utilizado menos recientemente). Los datos más antiguos se saltan de la memoria caché cuando alcanza la capacidad.

Probarlo implica establecer la capacidad en un número relativamente pequeño durante la prueba, exceder la capacidad y garantizar que los datos más antiguos ya no estén en la memoria caché y acceder a ella requiere acceso a la base de datos.

Probando su Integridad de Datos

Su sistema es tan bueno como su integridad de datos. Si tiene datos corruptos o datos faltantes, entonces está en mal estado. En los sistemas del mundo real, es difícil mantener una integridad de datos perfecta. El esquema y los formatos cambian, los datos se ingieren a través de canales que pueden no verificar todas las restricciones, los errores ingresan los datos incorrectos, los administradores intentan correcciones manuales, las copias de seguridad y las restauraciones pueden no ser confiables.

Dada esta dura realidad, debes probar la integridad de los datos de tu sistema. Probar la integridad de los datos es diferente de las pruebas automatizadas regulares después de cada cambio de código. La razón es que los datos pueden salir mal incluso si el código no cambió. Definitivamente desea ejecutar comprobaciones de integridad de datos después de cambios de código que podrían alterar el almacenamiento o la representación de datos, pero también ejecutarlos periódicamente.

Restricciones de prueba

Las restricciones son la base de su modelado de datos. Si usa una base de datos relacional, puede definir algunas restricciones en el nivel SQL y dejar que el DB las aplique. La nulidad, la longitud de los campos de texto, la singularidad y las relaciones 1-N se pueden definir fácilmente. Pero SQL no puede verificar todas las restricciones.

Por ejemplo, en Desongcious, hay una relación N-N entre usuarios y canciones. Cada canción debe estar asociada con al menos un usuario. No hay una buena forma de aplicar esto en SQL (bueno, puede tener una clave externa de una canción a otra y hacer que la canción apunte a uno de los usuarios asociados con ella). Otra restricción puede ser que cada usuario puede tener como máximo 500 canciones. Nuevamente, no hay forma de representarlo en SQL. Si utiliza almacenes de datos NoSQL, generalmente hay incluso menos soporte para declarar y validar restricciones en el nivel del almacén de datos.

Eso te deja con un par de opciones:

  • Asegúrese de que el acceso a los datos se realice solo a través de interfaces y herramientas probadas que apliquen todas las restricciones.
  • Examine periódicamente sus datos, busque violaciones de restricciones y corrígelos.

Prueba de Idempotencia

Idempotencia significa que realizar la misma operación varias veces seguidas tendrá el mismo efecto que realizarla una vez.

Por ejemplo, establecer la variable x a 5 es idempotente. Puede establecer x a 5 una vez o un millón de veces. Todavía será 5. Sin embargo, aumentar X por 1 no es idempotente. Cada incremento consecutivo cambia su valor. Idempotency es una propiedad muy deseable en sistemas distribuidos con particiones temporales de red y protocolos de recuperación que intentan enviar un mensaje varias veces si no hay una respuesta inmediata.

Si diseña idempotencia en su código de acceso a datos, debe probarlo. Esto es típicamente muy fácil. Para cada operación idempotente se extiende para realizar la operación dos o más veces seguidas y verificar que no haya errores y que el estado siga siendo el mismo.

Tenga en cuenta que el diseño idempotente a veces puede ocultar errores. Considere eliminar un registro de un DB. Es una operación idempotente. Después de eliminar un registro, el registro ya no existe en el sistema, y ​​tratar de eliminarlo nuevamente no lo recuperará. Eso significa que intentar eliminar un registro inexistente es una operación válida. Pero podría enmascarar el hecho de que la persona que llamó aprobó la clave de registro incorrecta. Si devuelve un mensaje de error, entonces no es idempotente.

Probar migraciones de datos

Las migraciones de datos pueden ser operaciones muy riesgosas. Algunas veces ejecuta un script sobre todos sus datos o partes críticas de sus datos y realiza alguna cirugía seria. Debería estar preparado con el plan B en caso de que algo salga mal (por ejemplo, vuelva a los datos originales y descubra qué salió mal).

En muchos casos, la migración de datos puede ser una operación lenta y costosa que puede requerir dos sistemas uno al lado del otro durante la migración. Participé en varias migraciones de datos que tomaron varios días o incluso semanas. Cuando se enfrenta a una migración masiva de datos, vale la pena invertir el tiempo y probar la migración en un subconjunto pequeño (pero representativo) de sus datos y luego verificar que los datos recién migrados sean válidos y que el sistema pueda trabajar con ellos.

Prueba de datos perdidos

La falta de datos es un problema interesante. A veces, los datos faltantes violarán la integridad de los datos (por ejemplo, una canción cuyo usuario falta) y, a veces, solo faltan (por ejemplo, si alguien elimina a un usuario y todas sus canciones).

Si los datos faltantes causan un problema de integridad de datos, entonces lo detectará en sus pruebas de integridad de datos. Sin embargo, si faltan algunos datos, entonces no hay una forma fácil de detectarlo. Si los datos nunca se convirtieron en almacenamiento persistente, entonces tal vez haya un rastro en los registros u otras tiendas temporales.

Dependiendo de la cantidad de datos que faltan de riesgo, puede escribir algunas pruebas que eliminen deliberadamente algunos datos de su sistema y verifiquen que el sistema se comporte como se espera.

Conclusion

Probar el código de datos intensivos requiere una planificación deliberada y una comprensión de sus requisitos de calidad. Puede probar en varios niveles de abstracción, y sus elecciones afectarán cuán exhaustivas e integrales son sus pruebas, cuántos aspectos de su capa de datos real prueba, qué tan rápido se ejecutan sus pruebas, y qué tan fácil es modificar sus pruebas cuando el cambios en la capa de datos

No hay una sola respuesta correcta. Necesita encontrar su punto óptimo en todo el espectro, desde pruebas súper integrales, lentas y laboriosas hasta pruebas rápidas y livianas.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.