Pruebas de integración paralela Postgresql en la aplicación GO


Las pruebas de integración son uno de los niveles de la pirámide de pruebas . Usualmente requieren más tiempo, porque en ellos no reemplazamos nada con simulaciones de componentes reales. Para reducir el tiempo de tales pruebas, podemos ejecutarlas en paralelo. Aquí hablaré específicamente sobre tales pruebas para Postgresql.

Idealmente, cada prueba debe ser independiente, para que no se afecten entre sí. En otras palabras, cada función de prueba tiene su propio estado. Esta es una buena señal para usar pruebas paralelas. Para obtener mi conjunto de datos personales para cada función de prueba, creé una función que, al comenzar una prueba, crea un circuito temporal, carga datos en él y destruye el circuito una vez que se completa la prueba. Cada esquema creado contiene un hash en el nombre para evitar conflictos de nombres.


Función auxiliar


Comencemos con una función auxiliar para mostrar errores en las pruebas. Tomé las funciones de ayuda de Ben Johnson (Ben Johnson), que me ayudaron a guardar algunas líneas de código y hacer mis errores más claros y detallados.

Datos de prueba


Para ejecutar la prueba de integración de la base de datos, se deben proporcionar datos de prueba. La herramienta de prueba Go tiene un buen soporte para cargar datos de prueba desde archivos. Primero, vaya a las carpetas de saltos de compilación llamadas "testdata". En segundo lugar, cuando ejecuta "ir a prueba", cambia la carpeta actual a la carpeta del paquete. Esto le permite utilizar la ruta relativa a la carpeta testdata para cargar el conjunto de datos de prueba.

Crear una conexión de base de datos para la prueba


package database import ( "math/rand" "strconv" "testing" "time" _ "github.com/lib/pq" "database/sql" ) const ( dbPort = 5439 dbUser = "postgres" dbPassword = "postgres" dbName = "test" ) func CreateTestDatabase(t *testing.T) (*sql.DB, string, func()) { connectionString := fmt.Sprintf("port=%d user=%s password=%s dbname=%s sslmode=disable", dbPort, dbUser, dbPassword, dbName) db, dbErr := sql.Open("postgres", connectionString) if dbErr != nil { t.Fatalf("Fail to create database. %s", dbErr.Error()) } rand.Seed(time.Now().UnixNano()) schemaName := "test" + strconv.FormatInt(rand.Int63(), 10) _, err := db.Exec("CREATE SCHEMA " + schemaName) if err != nil { t.Fatalf("Fail to create schema. %s", err.Error()) } return db, schemaName, func() { _, err := db.Exec("DROP SCHEMA " + schemaName + " CASCADE") if err != nil { t.Fatalf("Fail to drop database. %s", err.Error()) } } } 


Llamar a "CreateTestDatabase" para crear una conexión a la base de datos de prueba y crear un nuevo esquema de datos para las pruebas. Esta función devuelve la conexión de la base de datos, el nombre del esquema creado y la función de purga para eliminar este esquema. Para una prueba, es mejor fallar la prueba que devolver un error a la persona que llama. (Nota: El retorno de la función de limpieza se basa en las pruebas avanzadas de Mitchell Hashimoto con Go Talk ).

Descargar Test Dataset


Usé los archivos ".sql". Un (1) sql contiene datos para una (1) tabla. Incluye crear una tabla y llenarla con datos. Todos los archivos sql se almacenan en la carpeta "testdata". Aquí hay un archivo sql de ejemplo.

 CREATE TABLE book ( title character varying(50), author character varying(50) ); INSERT INTO book VALUES ('First Book','First Author'), ('Second Book','Second Author') ; 

Y aquí está la parte intrincada. Debido a que cada función se ejecuta en su propio esquema de datos único, no podemos simplemente ejecutar (escribir) una consulta en estos archivos sql. Debemos especificar el esquema antes de los nombres de las tablas para crear una tabla o insertar datos en el esquema temporal deseado. Por ejemplo, el libro CREATE TABLE ... debe escribirse como CREATE TABLE uniqueschema.book ... y el libro INSERT INTO ... debe cambiarse a INSERT INTO uniqueschema.book ... Utilicé expresiones regulares para modificar consultas antes de ejecutar. Aquí está el código de descarga de datos de prueba:

 package datalayer import ( "bufio" "fmt" "io" "os" "regexp" "testing" "database/sql" "github.com/Hendra-Huang/databaseintegrationtest/testingutil" //     (   ,  79) ) //        var schemaPrefixRegexps = [...]*regexp.Regexp{ regexp.MustCompile(`(?i)(^CREATE SEQUENCE\s)(["\w]+)(.*)`), regexp.MustCompile(`(?i)(^CREATE TABLE\s)(["\w]+)(\s.+)`), regexp.MustCompile(`(?i)(^ALTER TABLE\s)(["\w]+)(\s.+)`), regexp.MustCompile(`(?i)(^UPDATE\s)(["\w]+)(\s.+)`), regexp.MustCompile(`(?i)(^INSERT INTO\s)(["\w]+)(\s.+)`), regexp.MustCompile(`(?i)(^DELETE FROM\s)(["\w]+)(.*)`), regexp.MustCompile(`(?i)(.+\sFROM\s)(["\w]+)(.*)`), regexp.MustCompile(`(?i)(\sJOIN\s)(["\w]+)(.*)`), } //      func addSchemaPrefix(schemaName, query string) string { prefixedQuery := query for _, re := range schemaPrefixRegexps { prefixedQuery = re.ReplaceAllString(prefixedQuery, fmt.Sprintf("${1}%s.${2}${3}", schemaName)) } return prefixedQuery } func loadTestData(t *testing.T, db *sql.DB, schemaName string, testDataNames ...string) { for _, testDataName := range testDataNames { file, err := os.Open(fmt.Sprintf("./testdata/%s.sql", testDataName)) testingutil.Ok(t, err) reader := bufio.NewReader(file) var query string for { line, err := reader.ReadString('\n') if err == io.EOF { break } testingutil.Ok(t, err) line = line[:len(line)-1] if line == "" { query = addSchemaPrefix(schemaName, query) _, err := db.Exec(query) testingutil.Ok(t, err) query = "" } query += line } file.Close() } } 


Prueba de creación


Antes de comenzar cada prueba, se creará una base de datos de prueba con un nombre único para el esquema y se pospondrá la ejecución de la función de limpieza para eliminar este esquema. El nombre del esquema se insertará en la solicitud en la prueba. Lo más importante en esta implementación es que la conexión de la base de datos debe ser personalizable para cambiar la conexión de la base de datos real a la conexión con la base de datos de prueba. Agregue "t.Parallel ()" al comienzo de cada función de prueba para indicar al entorno de prueba la necesidad de ejecutar esta prueba en paralelo.
A continuación se muestra el código completo:

 //            "integration" (. build flags) // +build integration package datalayer import ( "context" "testing" "github.com/Hendra-Huang/databaseintegrationtest/database" "github.com/Hendra-Huang/databaseintegrationtest/testingutil" ) func TestInsertBook(t *testing.T) { t.Parallel() db, schemaName, cleanup := database.CreateTestDatabase(t) defer cleanup() loadTestData(t, db, schemaName, "book") // will load data which the filename is book title := "New title" author := "New author" // those 2 lines code below are not a good practice // but it is intentional to keep the focus only on integration test part // the important part is database connection has to be configurable insertBookQuery = addSchemaPrefix(schemaName, insertBookQuery) // override the query and add schema to the query err := InsertBook(context.Background(), db, title, author) // will execute insertBookQuery with the provided connection testingutil.Ok(t, err) } func TestGetBooks(t *testing.T) { t.Parallel() db, schemaName, cleanup := database.CreateTestDatabase(t) defer cleanup() loadTestData(t, db, schemaName, "book") getBooksQuery = addSchemaPrefix(schemaName, getBooksQuery) books, err := GetBooks(context.Background(), db) testingutil.Ok(t, err) testingutil.Equals(t, 2, len(books)) } 


Nota: En "TestGetBooks", supongo que la consulta devolverá 2 libros, como He dado muchos datos de prueba establecidos en "testdata / book.sql" aunque hay una prueba de inserción arriba. Si no compartimos el circuito entre las dos pruebas, "TestGetBooks" fallará, porque ahora 3 filas en la tabla, 2 de la prueba, 1 del inserto de prueba anterior. Esta es la ventaja de los circuitos separados para las pruebas: sus datos son independientes y, por lo tanto, las pruebas son independientes entre sí.

El ejemplo del proyecto que publiqué aquí github . Puede copiarlo usted mismo, ejecutar la prueba y ver el resultado.

Conclusión


Para mi proyecto, este enfoque reduce el tiempo de prueba en un 40–50%, en comparación con las pruebas secuenciales. Otra ventaja de ejecutar pruebas en paralelo es que podemos evitar algunos errores que pueden ocurrir cuando una aplicación procesa varias acciones competitivas.

Que tengas una buena prueba.

- Imagen de medium.com/kongkow-it-medan/parallel-database-integration-test-on-go-application-8706b150ee2e

Source: https://habr.com/ru/post/466459/


All Articles