Prueba de código intensivo de datos con Go, Parte 2

Visión general

Esta es la parte dos de cinco en una serie de tutoriales sobre pruebas de código de uso intensivo de datos. En la primera parte, cubrí el diseño de una capa de datos abstractos que permite realizar pruebas adecuadas, cómo manejar los errores en la capa de datos, cómo simular el código de acceso a los datos y cómo realizar una prueba con una capa de datos abstracta. En este tutorial, analizaré una capa de datos en memoria real basada en el popular SQLite. 

Pruebas contra un almacén de datos en memoria

Las pruebas contra una capa de datos abstractos son excelentes para algunos casos de uso donde se necesita mucha precisión, entiendes exactamente qué llamadas hará el código bajo prueba contra la capa de datos y estás de acuerdo con preparar las respuestas simuladas.

A veces, no es tan fácil. La serie de llamadas a la capa de datos puede ser difícil de calcular, o requiere mucho esfuerzo preparar las respuestas enlatadas adecuadas que sean válidas. En estos casos, es posible que deba trabajar con un almacén de datos en memoria. 

Los beneficios de un almacén de datos en memoria son:

  • Es muy rápido. 
  • Trabajas contra un almacén de datos real..
  • A menudo se puede rellenar desde cero utilizando archivos o código.

En particular, si su almacén de datos es un DB relacional, SQLite es una opción fantástica. Solo recuerde que hay diferencias entre SQLite y otros DBs relacionales populares como MySQL y PostgreSQL.

Asegúrate de tenerlo en cuenta en tus pruebas. Tenga en cuenta que todavía accede a sus datos a través de la capa de datos abstractos, pero ahora el almacén de respaldo durante las pruebas es el almacén de datos en memoria. Su prueba llenará los datos de la prueba de manera diferente, pero el código bajo prueba no tiene conocimiento de lo que está pasando.

Utilizando SQLite

SQLite es un DB embebido (vinculado a su aplicación). No hay un servidor de base de datos separado ejecutándose Normalmente almacena los datos en un archivo, pero también tiene la opción de un almacén de respaldo en memoria. 

Aquí está el EnMemoryDataStore estructura También es parte de la concrete_data_layer paquete, e importa el paquete de terceros go-sqlite3 que implementa la interfaz estándar "base de datos / sql" de Golang.  

package concrete_data_layer import ("database / sql". "abstract_data_layer" _ "github.com/mattn/go-sqlite3" "time" "fmt") tipo InMemoryDataLayer struct db * sql.DB

Construyendo la capa de datos en memoria

los NewInMemoryDataLayer () La función constructora crea una base de datos sqlite en memoria y devuelve un puntero a la InMemoryDataLayer

func NewInMemoryDataLayer () (* InMemoryDataLayer, error) db, err: = sql.Open ("sqlite3", ": memory:") if err! = nil return nil, err err = createSqliteSchema (db) return & InMemoryDataLayer  db nil 

Tenga en cuenta que cada vez que abre una nueva base de datos ": memory:", comienza desde cero. Si quieres persistencia a través de múltiples llamadas a NewInMemoryDataLayer (), Deberías usar file :: memory:? cache = shared. Vea este hilo de discusión de GitHub para más detalles..

los InMemoryDataLayer implementa el DataLayer Interfaz y en realidad almacena los datos con las relaciones correctas en su base de datos sqlite. Para hacer eso, primero debemos crear un esquema adecuado, que es exactamente el trabajo de createSqliteSchema () Funcionar en el constructor. Crea tres tablas de datos: canción, usuario y etiqueta, y dos tablas de referencias cruzadas, label_song y usuario_song.

Agrega algunas restricciones, índices y claves externas para relacionar las tablas entre sí. No voy a detenerme en los detalles específicos. La esencia de esto es que todo el esquema DDL se declara como una sola cadena (que consta de varias declaraciones DDL) que luego se ejecutan utilizando el db.Exec () Método, y si algo sale mal, devuelve un error.. 

func createSqliteSchema (db * sql.DB) error schema: = 'CREATE TABLE SI NO EXISTE canción (id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE, nombre TEXT, descripción TEXT); CREAR TABLA SI NO EXISTE usuario (id INTEGER PRIMARY KEY AUTOINCREMENT, nombre TEXT, correo electrónico TEXT UNIQUE, registered_at TIMESTAMP, last_login TIMESTAMP); CREAR ÍNDICE user_email_idx ON usuario (correo electrónico); CREAR TABLA SI NO EXISTE la etiqueta (ID INTEGER PRIMARY KEY AUTOINCREMENT, nombre TEXT UNIQUE); CREAR ÍNDICE label_name_idx ON label (nombre); CREAR TABLA SI NO EXISTE label_song (label_id INTEGER NOT NULL REFERENCES label (id), song_id INTEGER NOT NULL REFERENCES song (ID), PRIMARY KEY (label_id, song_id)); CREAR TABLA SI NO EXISTE user_song (user_id INTEGER NOT NULL REFERENCES user (id), song_id INTEGER NOT NULL REFERENCES song (ID), PRIMARY KEY (user_id, song_id)); ' _, err: = db.Exec (schema) return err 

Es importante darse cuenta de que, mientras que SQL es estándar, cada sistema de administración de bases de datos (DBMS) tiene su propio sabor, y la definición exacta del esquema no necesariamente funcionará como está para otra base de datos..

Implementando la capa de datos en memoria

Para darle una idea del esfuerzo de implementación de una capa de datos en memoria, aquí hay un par de métodos: AddSong () y GetSongsByUser ()

los AddSong () El método hace mucho trabajo. Inserta un registro en el canción tabla, así como en cada una de las tablas de referencia: label_song y usuario_song. En cada punto, si alguna operación falla, simplemente devuelve un error. No uso ninguna transacción porque está diseñada solo para fines de prueba, y no me preocupo por datos parciales en la base de datos.

func (m * InMemoryDataLayer) AddSong (usuario usuario, canción Canción, etiqueta [] Etiqueta) error s: = 'INSERTAR EN la canción (url, nombre, descripción) valores (?,?,?)' declaración, err: = m .db.Prepare (s) if err! = nil return err result, err: = statement.Exec (song.Url, song.Name, song.Description) if err! = nil return err songId, err: = result.LastInsertId () if err! = nil return err s = "SELECT id FROM del usuario donde email =?" filas, err: = m.db.Query (s, user.Email) if err! = nil return err var userId int para rows.Next () err = rows.Scan (& userId) si err! = nil  return err s = 'INSERT INTO user_song (user_id, song_id) valores (?,?)' sentencia, err = m.db.Prepare (s) si err! = nil return err _, err = statement.Exec (userId, songId) if err! = nil return err var labelId int64 s: = "INSERT INTO valores de etiqueta (nombre) (?)" label_ins, err: = m.db.Prepare (s) si err! = nil return err s = 'INSERT INTO label_song (label_id, song_id) valores (?,?)' label_song_ins, err: = m.db.Prepare (s) si err! = nil return err para _, t: = rango de etiquetas s = "SELECT id FROM label donde name =?" filas, err: = m.db.Query (s, t.Name) if err! = nil return err labelId = -1 para rows.Next () err = rows.Scan (& labelId) si err! = nil return err si labelId == -1 resultado, err = label_ins.Exec (t.Name) if err! = nil return err labelId, err = resultado.LastInsertId () if err! = nil return err  resultado, err = label_song_ins.Exec (labelId, songId) if err! = nil return err return nil 

los GetSongsByUser () utiliza una combinación + sub-selección de la usuario_song referencia cruzada para devolver canciones para un usuario específico. Utiliza el Consulta() métodos y luego escanea cada fila para rellenar un Canción Realice una estructura desde el modelo de objeto de dominio y devuelva una porción de canciones. La implementación de bajo nivel como un DB relacional se oculta de manera segura.

func (m * InMemoryDataLayer) GetSongsByUser (u User) ([] Song, error) s: = 'SELECCIONAR url, título, descripción DE la canción L INNER JOIN user_song UL ON UL.song_id = L.id WHERE UL.user_id = ( SELECCIONE la identificación del usuario WHERE email =?) 'Filas, err: = m.db.Query (s, u.Email) if err! = Nil return nil, err para rows.Next () var song Song err = rows.Scan (& song.Url, & song.Title, & song.Description) if err! = nil return nil, err songs = append (songs, song) return songs, nil 

Este es un gran ejemplo de la utilización de una base de datos relacional real como sqlite para implementar el almacén de datos en memoria en lugar de rodar la nuestra, lo que requeriría mantener mapas y garantizar que toda la contabilidad sea correcta. 

Ejecutando Pruebas Contra SQLite

Ahora que tenemos una capa de datos en memoria adecuada, echemos un vistazo a las pruebas. Coloqué estas pruebas en un paquete separado llamado sqlite_test, e importo localmente la capa de datos abstractos (el modelo de dominio), la capa de datos concretos (para crear la capa de datos en memoria) y el administrador de canciones (el código bajo prueba). También preparo dos canciones para las pruebas del sensacional artista panameño El Chombo.!

package sqlite_test import ("testing". "abstract_data_layer". "concrete_data_layer". "song_manager") const (url1 = "https://www.youtube.com/watch?v=MlW7T0SUH0E" url2 = "https: // www. youtube.com/watch?v=cVFDlg4pbwM ") var testSong = Song Url: url1, Nombre:" Chacaron " var testSong2 = Song Url: url2, Nombre:" El Gato Volador " 

Los métodos de prueba crean una nueva capa de datos en memoria para comenzar desde cero y ahora pueden invocar métodos en la capa de datos para preparar el entorno de prueba. Cuando todo está configurado, pueden invocar los métodos del administrador de canciones y luego verificar que la capa de datos contenga el estado esperado..

Por ejemplo, el AddSong_Success () método de prueba crea un usuario, agrega una canción usando el administrador de canciones AddSong () Método, y verifica que las llamadas posteriores. GetSongsByUser () devuelve la canción añadida. Luego agrega otra canción y verifica de nuevo..

func TestAddSong_Success (t * testing.T) u: = User Nombre: "Gigi", Correo electrónico: "[email protected]" dl, err: = NewInMemoryDataLayer () if err! = nil t.Error (" Error al crear la capa de datos en memoria ") err = dl.CreateUser (u) si err! = Nil t.Error (" Error al crear usuario ") lm, err: = NewSongManager (u, dl) si err ! = nil t.Error ("NewSongManager () devolvió 'nil'") err = lm.AddSong (testSong, nil) si err! = nil t.Error ("AddSong () failed") canciones, err : = dl.GetSongsByUser (u) if err! = nil t.Error ("GetSongsByUser () falla") if len (songs)! = 1 t.Error ('GetSongsByUser () no devolvió una canción como esperado ') si las canciones [0]! = testSong t.Error ("La canción agregada no coincide con la canción de entrada") // Agrega otra canción err = lm.AddSong (testSong2, nil) si err! = nil  t.Error ("AddSong () failed") canciones, err = dl.GetSongsByUser (u) if err! = nil t.Error ("GetSongsByUser () failed") if len (songs)! = 2 t .Error ('GetSongsByUser () no devolvió dos canciones como se esperaba') si las canciones [0]! = TestSong t.Error ("Se agregó la canción no coincide con la canción de entrada ") si las canciones [1]! = testSong2 t.Error (" La canción agregada no coincide con la canción de entrada ") 

los TestAddSong_Duplicate () el método de prueba es similar, pero en lugar de agregar una nueva canción la segunda vez, agrega la misma canción, lo que resulta en un error de canción duplicada:

 u: = Usuario Nombre: "Gigi", Correo electrónico: "[email protected]" dl, err: = NewInMemoryDataLayer () if err! = nil t.Error ("No se pudo crear la capa de datos en la memoria")  err = dl.CreateUser (u) if err! = nil t.Error ("Error al crear usuario") lm, err: = NewSongManager (u, dl) si err! = nil t.Error ("NewSongManager () devolvió 'nil' ") err = lm.AddSong (testSong, nil) if err! = nil t.Error (" AddSong () failed ") songs, err: = dl.GetSongsByUser (u) if err ! = nil t.Error ("GetSongsByUser () falla") si len (canciones)! = 1 t.Error ('GetSongsByUser () no devolvió una canción como se esperaba') si las canciones [0]! = testSong t.Error ("La canción agregada no coincide con la canción de entrada") // Agregar nuevamente la misma canción err = lm.AddSong (testSong, nil) si err == nil t.Error ('AddSong () debería haber fallado para una canción duplicada ') expectedErrorMsg: = "Canción duplicada" errorMsg: = err.Error () si errorMsg! = expectedErrorMsg t.Error (' AddSong () devolvió un mensaje de error incorrecto para la canción duplicada))

Conclusión

En este tutorial, implementamos una capa de datos en memoria basada en SQLite, completamos una base de datos SQLite en memoria con datos de prueba y utilizamos la capa de datos en memoria para probar la aplicación.

En la tercera parte, nos centraremos en las pruebas contra una capa de datos complejos locales que consta de múltiples almacenes de datos (una base de datos relacional y un caché de Redis). Manténganse al tanto.