Esta es la parte cinco de cinco en una serie de tutoriales sobre pruebas de código de uso intensivo de datos. En la parte cuatro, traté los almacenes de datos remotos, utilizando bases de datos de prueba compartidas, utilizando instantáneas de datos de producción y generando sus propios datos de prueba. En este tutorial, repasaré las pruebas fuzz, las pruebas de su caché, las pruebas de integridad de los datos, las pruebas de idempotencia y los datos faltantes.
La idea de las pruebas fuzz es abrumar al sistema con mucha información aleatoria. En lugar de intentar pensar en una entrada que cubra todos los casos, lo que puede ser difícil y / o muy laborioso, deja que la oportunidad lo haga por usted. Conceptualmente es similar a la generación de datos aleatorios, pero la intención aquí es generar entradas aleatorias o semi-aleatorias en lugar de datos persistentes.
La prueba Fuzz es útil en particular para detectar problemas de seguridad y rendimiento cuando las entradas inesperadas causan bloqueos o pérdidas de memoria. Pero también puede ayudar a garantizar que todas las entradas no válidas se detecten antes y sean rechazadas correctamente por el sistema.
Considere, por ejemplo, la entrada que viene en forma de documentos JSON profundamente anidados (muy comunes en las API web). Tratar de 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..
Hay varias bibliotecas que puedes usar para las pruebas fuzz. Mi favorito es el gofuzz de Google. Aquí hay un ejemplo simple que genera automáticamente 200 objetos únicos de una estructura con varios campos, incluyendo una estructura anidada.
import ("fmt" "github.com/google/gofuzz") func SimpleFuzzing () type SomeType struct A string B string C int D struct E float64 f: = fuzz.New () object: = SomeType uniqueObjects: = map [SomeType] int para i: = 0; yo < 200; i++ f.Fuzz(&object) uniqueObjects[object]++ fmt.Printf("Got %v unique objects.\n", len(uniqueObjects)) // Output: // Got 200 unique objects.
Casi todos los sistemas complejos que tratan con 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 informática: nombrar cosas, invalidar la caché y desactivar uno por uno.
Bromas aparte, administrar su estrategia e implementación de almacenamiento en caché puede complicar su acceso a los datos, pero puede tener un impacto tremendo en el costo y el rendimiento de su acceso a los datos. La prueba de su caché no se puede hacer 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 del caché de la capa de datos híbridos de Songify.
Los cachés viven y mueren por su éxito / falta de rendimiento. La funcionalidad básica de una memoria caché es que si los datos solicitados están disponibles en la memoria caché (un resultado), se recuperarán de la memoria caché y no del almacén de datos principal. En el diseño original de la HybridDataLayer
, El acceso al caché se realizó mediante métodos privados..
Las reglas de visibilidad de Go hacen que sea imposible llamarlos directamente o reemplazarlos desde 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 DataLayer
interfaz, 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 Redis, para poder manipular el caché:
func (m * HybridDataLayer) GetRedis () * redis.Client return m.redis
A continuación voy a cambiar el getSongByUser_DB ()
Métodos para una variable de función pública. Ahora, en la prueba, puedo reemplazar el GetSongsByUser_DB ()
variable con una función que realiza un seguimiento de cuántas veces se llamó y luego la reenvía a la función original. Eso nos permite verificar si una llamada a GetSongsByUser ()
Recuperé las canciones del caché o de la base de datos..
Vamos a desglosarlo pieza por pieza. Primero, obtenemos la capa de datos (que también borra la base de datos y la redisección), creamos un usuario y agregamos una canción. los AddSong ()
método también rellena redis.
func TestGetSongsByUser_Cache (t * testing.T) now: = time.Now () u: = User Name: "Gigi", correo electrónico: "[email protected]", RegisteredAt: now, LastLogin: now dl, err : = getDataLayer () if err! = nil t.Error ("No se pudo crear la capa de datos híbrida") err = dl.CreateUser (u) if err! = nil t.Error ("No se pudo crear el usuario") lm, err: = NewSongManager (u, dl) si err! = nil t.Error ("NewSongManager () devolvió 'nil'") err = lm.AddSong (testSong, nil) si err! = nil t Error ("Error en AddSong ()")
Esta es la parte genial. Conservo la función original y defino una nueva función instrumentada que incrementa el local callCount
variable (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.
callCount: = 0 originalFunc: = GetSongsByUser_DB instrumentedFunc: = func (m * HybridDataLayer, cadena de correo electrónico, canciones * [] Song) (error de error) callCount + = 1 retorno originalFunc (m, correo electrónico, canciones) GetSongsByUser_DB = instrumentedFunc
En este punto, estamos listos para probar la operación de caché. Primero, la prueba llama al GetSongsByUser ()
del SongManager
Eso lo reenvía a la capa de datos híbrida. El caché se debe llenar para este usuario que acabamos de agregar. Así que el resultado esperado es que nuestra función instrumentada no será llamada, y el callCount
permanecerá en cero.
_, err = lm.GetSongsByUser (u) if err! = nil t.Error ("Error en GetSongsByUser ()") // Verifique que no se haya accedido al DB porque el cache debe ser // completado por AddSong () si callCount > 0 t.Error ('GetSongsByUser_DB () llamado cuando no debería tener')
El último caso de prueba es garantizar que si los datos del usuario no están en el caché, se recuperarán correctamente de la base de datos. La prueba lo lleva a cabo limpiando Redis (borrando todos sus datos) y haciendo otra llamada a GetSongsByUser ()
. Esta vez, se llamará a la función instrumentada, y la prueba verifica que el callCount
es igual a 1. Finalmente, el original GetSongsByUser_DB ()
la función es restaurada.
// Borre el caché dl.GetRedis (). FlushDB () // Obtenga las canciones nuevamente, ahora debería ir a la base de datos // porque la caché está vacía _, err = lm.GetSongsByUser (u) si err! = Nil t.Error ("GetSongsByUser () falla") // Verifique que se haya accedido a la base de datos porque el caché está vacío si callCount! = 1 t.Error ('GetSongsByUser_DB () no se llamó una vez como debería haberlo hecho') GetSongsByUser_DB = originalFunc
Nuestro caché es muy básico y no hace ninguna invalidación. Esto funciona bastante bien siempre y cuando todas las canciones se agreguen a través del AddSong ()
Método que se encarga de actualizar Redis. Si agregamos más operaciones, como eliminar canciones o eliminar usuarios, estas operaciones deberían encargarse de actualizar Redis en consecuencia.
Este cache muy simple funcionará incluso si tenemos un sistema distribuido en el que varias máquinas independientes puedan ejecutar nuestro servicio Songify, siempre que todas las instancias funcionen con las mismas instancias de DB y Redis..
Sin embargo, si la base de datos y el caché pueden desincronizarse debido a las operaciones de mantenimiento u otras herramientas y aplicaciones que cambian nuestros datos, entonces debemos crear una política de invalidación y actualización para el caché. Puede probarse utilizando las mismas técnicas: reemplazar funciones de destino o acceder directamente a la base de datos y a Redis en su prueba para verificar el estado.
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é son los cachés LRU (usados menos recientemente). Los datos más antiguos se eliminan del caché cuando alcanza su capacidad.
La prueba implica establecer la capacidad en un número relativamente pequeño durante la prueba, superar la capacidad y asegurar 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.
Su sistema es tan bueno como la integridad de sus datos. Si tiene datos dañados o faltan datos, entonces está en mala forma. En los sistemas del mundo real, es difícil mantener una perfecta integridad de los datos. El esquema y los formatos cambian, los datos se ingieren a través de los canales que no pueden verificar todas las restricciones, los errores permiten la entrada de datos incorrectos, los administradores intentan reparaciones manuales, las copias de seguridad y las restauraciones pueden ser poco confiables.
Dada esta dura realidad, debe probar la integridad de los datos de su sistema. La integridad de los datos de prueba es diferente de las pruebas automatizadas regulares después de cada cambio de código. La razón es que los datos pueden ir mal incluso si el código no cambió. Definitivamente desea ejecutar comprobaciones de integridad de datos después de los cambios de código que pueden alterar el almacenamiento o la representación de datos, pero también ejecutarlos periódicamente.
Las restricciones son la base de su modelado de datos. Si utiliza una base de datos relacional, puede definir algunas restricciones en el nivel de SQL y dejar que la base de datos las aplique. La nulidad, la longitud de los campos de texto, la unicidad y las relaciones 1-N se pueden definir fácilmente. Pero SQL no puede comprobar todas las restricciones.
Por ejemplo, en Desongcious, existe una relación N-N entre los usuarios y las canciones. Cada canción debe estar asociada con al menos un usuario. No hay una buena manera de imponer esto en SQL (bueno, puede tener una clave externa de la canción al usuario y hacer que la canción apunte a uno de los usuarios asociados a ella). Otra restricción puede ser que cada usuario tenga como máximo 500 canciones. Nuevamente, no hay manera de representarlo en SQL. Si utiliza los almacenes de datos NoSQL, generalmente hay menos soporte para declarar y validar restricciones en el nivel del almacén de datos..
Eso te deja con un par de opciones:
La 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 en 5 es idempotente. Puede establecer x a 5 una vez o un millón de veces. Seguirá siendo 5. Sin embargo, aumentar X en 1 no es idempotente. Cada incremento consecutivo cambia su valor. Idempotency es una propiedad muy deseable en sistemas distribuidos con particiones de red temporales y protocolos de recuperación que reintentan el envío de 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 suele ser muy fácil. Para cada operación idempotente, se extiende para realizar la operación dos veces o más en una fila y verificar que no haya errores y que el estado sigue siendo el mismo.
Tenga en cuenta que el diseño idempotente a veces puede ocultar errores. Considere eliminar un registro de una base de datos. 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 devolverá. Eso significa que intentar eliminar un registro no existente es una operación válida. Pero podría ocultar el hecho de que la persona que llamó pasó la clave de registro incorrecta. Si devuelve un mensaje de error, entonces no es idempotente.
Las migraciones de datos pueden ser operaciones muy arriesgadas. A veces ejecuta una secuencia de comandos sobre todos sus datos o partes críticas de sus datos y realiza una cirugía grave. Debería estar listo con el plan B en caso de que algo salga mal (por ejemplo, volver a los datos originales y averiguar qué fue lo que salió mal).
En muchos casos, la migración de datos puede ser una operación lenta y costosa que puede requerir dos sistemas en paralelo 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..
La falta de datos es un problema interesante. A veces, los datos faltantes violarán la integridad de sus datos (por ejemplo, una canción cuyo usuario falta), y a veces simplemente faltan (por ejemplo, alguien elimina un usuario y todas sus canciones).
Si los datos faltantes causan un problema de integridad de datos, los detectará en sus pruebas de integridad de datos. Sin embargo, si solo faltan algunos datos, no hay una manera fácil de detectarlos. Si los datos nunca se convirtieron en un almacenamiento persistente, tal vez haya un rastreo en los registros u otros almacenes temporales..
Dependiendo de la cantidad de riesgo que falten los datos, puede escribir algunas pruebas que eliminen deliberadamente algunos datos de su sistema y verificar que el sistema se comporta como se espera..
La prueba de código de uso intensivo de datos requiere una planificación deliberada y una comprensión de sus requisitos de calidad. Puede realizar pruebas en varios niveles de abstracción, y sus elecciones afectarán la exhaustividad y exhaustividad de sus pruebas, la cantidad de aspectos de su capa de datos real que realiza las pruebas, la rapidez con que se ejecutan las pruebas y la facilidad con que se modifican sus pruebas. cambios en la capa de datos.
No hay una sola respuesta correcta. Debe encontrar su punto óptimo en el espectro desde pruebas súper completas, lentas y que requieren mucha mano de obra hasta pruebas rápidas y livianas.