Skip to Content

Using GORM Hooks to Clean up Test Fixtures in Golang

If you’ve ever written code in Golang that interfaces with the database, chances are that you already know GORM. With GORM, creating, updating, deleting records is super simple.

package main
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
type Product struct {
gorm.Model
Code string
Price uint
}
func main() {
db, err := gorm.Open("sqlite3", "test.db")
if err != nil {
panic("failed to connect database")
}
defer db.Close()
// Migrate the schema
db.AutoMigrate(&Product{})
// Create
db.Create(&Product{Code: "L1212", Price: 1000})
// Read
var product Product
db.First(&product, 1) // find product with id 1
db.First(&product, "code = ?", "L1212") // find product with code l1212
// Update - update product's price to 2000
db.Model(&product).Update("Price", 2000)
// Delete - delete product
db.Delete(&product)
}
view raw main.go hosted with ❤ by GitHub

But GORM offers a lot more than just basic database operations. One of my favourites is the ability to attach database Hooks. Database hooks can be used to do all kinds of cool stuff like automatically deleting records created by test fixtures, logging information about when a record was inserted/deleted, updating records in one table when another is changed and so on.

Deleting Records Created by Test Fixtures with GORM

A common practice is to create test prerequisites via test fixtures. Let’s say you were testing an application which requires some data to be present in the database. The usual way of testing this would be to insert some test data in the database and then test the application. It is extremely important that the database is in a clean state between test runs and there are no leftovers in the database. You would never want the left over from the previous test affect your current test.

But there’s a problem — How do you remove all the records created by the test fixtures? You could drop the entire database but let’s assume you don’t want to do that. So how do you keep your test environment in a clean and consistent state?

One easy (not really!) and not-so-cool way of doing that would be to manually delete every record that you inserted in the database.

package main
import (
"database/sql"
"testing"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
type Product struct {
gorm.Model
Code string
Price uint
}
// Error checking intentionally skipped at some places
func TestCleanup(t *testing.T) {
db, _ := gorm.Open("sqlite3", "test.db")
defer db.Close()
// Create
db.Create(&Product{Code: "foo", Price: 100})
db.Create(&Product{Code: "bar", Price: 2000})
db.Create(&Product{Code: "fooBar", Price: 400})
// Perform some test here
// ...
// ...
var product Product
db.Delete(&product, "code = ?", "foo")
db.Delete(&product, "code = ?", "foo")
db.Delete(&product, "code = ?", "foo")
}
view raw cleanup_test.go hosted with ❤ by GitHub

So how you do delete records elegantly? GORM Hooks to the Rescue!

The easiest way is to set up a onCreate hook on the database and every time you create a record you store its information in a temporary data structure. Once your test completes, you can delete the all the records listed in the temporary data structure. The following code shows a simple function that can be used to remove all records created in the database after the hook was set up.

// DeleteCreatedEntities sets up GORM `onCreate` hook and return a function that can be deferred to
// remove all the entities created after the hook was set up
// You can use it as
//
// func TestSomething(t *testing.T){
// db, _ := gorm.Open(...)
//
// cleaner := DeleteCreatedEntities(db)
// defer cleaner()
//
// }
func DeleteCreatedEntities(db *gorm.DB) func() {
type entity struct {
table string
keyname string
key interface{}
}
var entries []entity
hookName := "cleanupHook"
db.Callback().Create().After("gorm:create").Register(hookName, func(scope *gorm.Scope) {
fmt.Printf("Inserted entities of %s with %s=%v\n", scope.TableName(), scope.PrimaryKey(), scope.PrimaryKeyValue())
entries = append(entries, entity{table: scope.TableName(), keyname: scope.PrimaryKey(), key: scope.PrimaryKeyValue()})
})
return func() {
// Remove the hook once we're done
defer db.Callback().Create().Remove(hookName)
// Find out if the current db object is already a transaction
_, inTransaction := db.CommonDB().(*sql.Tx)
tx := db
if !inTransaction {
tx = db.Begin()
}
// Loop from the end. It is important that we delete the entries in the
// reverse order of their insertion
for i := len(entries) - 1; i >= 0; i-- {
entry := entries[i]
fmt.Printf("Deleting entities from '%s' table with key %v\n", entry.table, entry.key)
tx.Table(entry.table).Where(entry.keyname+" = ?", entry.key).Delete("")
}
if !inTransaction {
tx.Commit()
}
}
}
view raw cleaner.go hosted with ❤ by GitHub

The entries array in the above snippet stores the list of records created after the hook was set up and these records would automatically be deleted when the returned function is called.

The following code snippet shows a more thorough example

package main
import (
"database/sql"
"fmt"
"testing"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
type Product struct {
gorm.Model
Code string
Price uint
}
// Error checking intentionally skipped at some places
func TestCleanup(t *testing.T) {
db, _ := gorm.Open("sqlite3", "test.db")
defer db.Close()
// Setup the cleaner
cleaner := DeleteCreatedEntities(db)
defer cleaner()
// Create
db.Create(&Product{Code: "foo", Price: 100})
db.Create(&Product{Code: "bar", Price: 2000})
db.Create(&Product{Code: "fooBar", Price: 400})
// Perform some tests here
// ...
// ...
}
func DeleteCreatedEntities(db *gorm.DB) func() {
type entity struct {
table string
keyname string
key interface{}
}
var entries []entity
hookName := "cleanupHook"
// Setup the onCreate Hook
db.Callback().Create().After("gorm:create").Register(hookName, func(scope *gorm.Scope) {
fmt.Printf("Inserted entities of %s with %s=%v\n", scope.TableName(), scope.PrimaryKey(), scope.PrimaryKeyValue())
entries = append(entries, entity{table: scope.TableName(), keyname: scope.PrimaryKey(), key: scope.PrimaryKeyValue()})
})
return func() {
// Remove the hook once we're done
defer db.Callback().Create().Remove(hookName)
// Find out if the current db object is already a transaction
_, inTransaction := db.CommonDB().(*sql.Tx)
tx := db
if !inTransaction {
tx = db.Begin()
}
// Loop from the end. It is important that we delete the entries in the
// reverse order of their insertion
for i := len(entries) - 1; i >= 0; i-- {
entry := entries[i]
fmt.Printf("Deleting entities from '%s' table with key %v\n", entry.table, entry.key)
tx.Table(entry.table).Where(entry.keyname+" = ?", entry.key).Delete("")
}
if !inTransaction {
tx.Commit()
}
}
}
view raw cleanup_test.go hosted with ❤ by GitHub

The test in the above code snippet creates 3 records in the database and the GORM hook stores those records in the entities array. Once the test is done, all the records are automatically deleted from the database. The following code snippet shows the output of the test in the code snippet above

➜ (foo) go test -v
=== RUN   TestCleanup
Inserted entities of products with id=40
Inserted entities of products with id=41
Inserted entities of products with id=42
Deleting entities from 'products' table with key 42
Deleting entities from 'products' table with key 41
Deleting entities from 'products' table with key 40
2019/01/27 15:12:12 [info] removing callback `cleanupHook` from /home/ijarif/Projects/go/src/github.com/jarifibrahim/foo/cleanup_test.go:81
--- PASS: TestCleanup (0.02s)
PASS
ok   github.com/jarifibrahim/foo 0.022s

This is how you can ensure your test environment remains in a clean and consistent state across test runs. If you’d like to read more about GORM hooks, head over to http://gorm.io/docs/hooks.html