RamSQL
Disposable SQL engine
RamSQL has been written to be used in your project's test suite.
Unit testing in Go is simple:
- create a
foo_test.go
- add
import testing
- add
func Test...(t *testing.T) {...}
- run
go test ./...
But then there are SQL queries, constraints, CRUD... and suddenly you need a PostgresSQL, setup scripts, and nothing is easy anymore.
The idea is to avoid setup, DBMS installation, and credentials management as long as possible.
A unique engine is tied to a single sql.DB with as much sql.Conn as needed providing a unique DataSourceName.
Bottom line: One DataSourceName per test and you have full test isolation in no time.
Installation
go get github.com/kokizzu/ramsql
Usage
Let's say you want to test the function LoadUserAddresses:
func LoadUserAddresses(db *sql.DB, userID int64) ([]string, error) {
query := `SELECT
address.street_number,
address.street
FROM address
JOIN user_addresses
ON address.id = user_addresses.address_id
WHERE user_addresses.user_id = $1;`
rows, err := db.Query(query, userID)
if err != nil {
return nil, err
}
var addresses []string
for rows.Next() {
var number int
var street string
if err := rows.Scan(&number, &street); err != nil {
return nil, err
}
addresses = append(addresses, fmt.Sprintf("%d %s", number, street))
}
return addresses, nil
}
Use RamSQL to test it in a disposable isolated in-memory SQL engine:
package myproject
import (
"database/sql"
"fmt"
"testing"
_ "github.com/kokizzu/ramsql/driver"
)
func TestLoadUserAddresses(t *testing.T) {
batch := []string{
`CREATE TABLE address (id BIGSERIAL PRIMARY KEY, street TEXT, street_number INT);`,
`CREATE TABLE user_addresses (address_id INT, user_id INT);`,
`INSERT INTO address (street, street_number) VALUES ('rue Victor Hugo', 32);`,
`INSERT INTO address (street, street_number) VALUES ('boulevard de la République', 23);`,
`INSERT INTO address (street, street_number) VALUES ('rue Charles Martel', 5);`,
`INSERT INTO address (street, street_number) VALUES ('chemin du bout du monde ', 323);`,
`INSERT INTO address (street, street_number) VALUES ('boulevard de la liberté', 2);`,
`INSERT INTO address (street, street_number) VALUES ('avenue des champs', 12);`,
`INSERT INTO user_addresses (address_id, user_id) VALUES (2, 1);`,
`INSERT INTO user_addresses (address_id, user_id) VALUES (4, 1);`,
`INSERT INTO user_addresses (address_id, user_id) VALUES (2, 2);`,
`INSERT INTO user_addresses (address_id, user_id) VALUES (2, 3);`,
`INSERT INTO user_addresses (address_id, user_id) VALUES (4, 4);`,
`INSERT INTO user_addresses (address_id, user_id) VALUES (4, 5);`,
}
db, err := sql.Open("ramsql", "TestLoadUserAddresses")
if err != nil {
t.Fatalf("sql.Open : Error : %s\n", err)
}
defer db.Close()
for _, b := range batch {
_, err = db.Exec(b)
if err != nil {
t.Fatalf("sql.Exec: Error: %s\n", err)
}
}
addresses, err := LoadUserAddresses(db, 1)
if err != nil {
t.Fatalf("Too bad! unexpected error: %s", err)
}
if len(addresses) != 2 {
t.Fatalf("Expected 2 addresses, got %d", len(addresses))
}
}
Done. No need for a running PostgreSQL or a setup. Your tests are isolated, and compliant with go tools.
RamSQL binary
Let's say you have a SQL describing your application structure:
CREATE TABLE IF NOT EXISTS address (id BIGSERIAL PRIMARY KEY, street TEXT, street_number INT);
CREATE TABLE IF NOT EXISTS user_addresses (address_id INT, user_id INT);
You may want to test its validity:
$ go install github.com/kokizzu/ramsql
$ ramsql < schema.sql
ramsql> Query OK. 1 rows affected
ramsql> Query OK. 1 rows affected
$ echo $?
0
Features
Unit testing
- Full isolation between tests
- No setup (either file or databases)
- Good performance
SQL parsing
- Databse schema validation
- ALTER file validation
Stress testing
- File system full error with configurable maximum database size
- Random configurable slow queries
- Random deconnection