Every SnailLife model struct has a repository struct to go with it. Since all repos are implementing the same Repository interface I wanted to reuse most of the code for testing them, but also allow for custom repo-specific tests. For example,OwnerRepo
does some stuff that StableRepo
does not - like optionally retrieving stables belonging to an owner.
For now I am going about it as follows, but any feedback or suggestions for a better way are more than welcome. Only a few bits and pieces of the actual functionality are implemented right now, and only for owners and stables, so that’s what we’re testing.
To do: test isolation
These are not unit tests - they work against actual test data and I am not mocking the db or the database package running against the db. So I do not expect them to be completely independent of other systems. However, what I do think is a big problem is that the tests are currently not isolated from each other. If the InsertOne test fails, the GetMany tests will fail. I do want to fail the test as early as possible (ie if InsertOne fails there is no point moving on to GetMany), but I don’t think having inter-dependent tests is the right way to achieve that - especially as test run order is not guaranteed! Up next I’ll work on checking if the db already has the records we need (which would be the case if Insert tests run first), and if not seed it with the necessary data for the Get tests, and then delete those rows after the Get test is done.
Create a repoTest
interface
It is an unexported interface in my tests
package (which lives under the domain
package). I also define a test
struct in the same file as well as a couple of the test case types we expect.
package tests
import (
"gitlab.com/drakonka/gosnaillife/server/lib/infrastructure/databases"
"gitlab.com/drakonka/gosnaillife/server/lib/infrastructure/repo"
"testing"
)
type repoTest interface {
prep() error
createInsertOneTestCases() []insertOneTestCase
createGetManyTestCases() []getManyTestCase
createRepo(db databases.Database) repo.Repo
createModel(params map[string]interface{}) repo.Model
getIdKey() string
getAdditionalTests() []func(t *testing.T)
}
type test struct {
tablename string
db databases.Database
idKey string
}
func (t *test) getIdKey() string {
return t.idKey
}
type insertOneTestCase struct {
tcname string
params map[string]interface{}
want error
}
type getManyTestCase struct {
tcname string
where string
params []interface{}
fields []string
want int
}
ownerTest
and stableTest
ownerTest
and stableTest
implement repoTest
and both have test
as an anonymous field. For the most part contain no actual test logic - all they do is prep the test db and define the test cases we want to run. Eg, ownerTest
defines createGetManyTestCases()
as follows:
func (ot *ownerTest) createGetManyTestCases() []getManyTestCase {
return []getManyTestCase{
{"OwnerGetOneWithAllProperties", "owner_id=?", []interface{}{10}, []string{}, 1},
{"OwnerGetOneWithOneProperty", "owner_id=?", []interface{}{10}, []string{"firstname"}, 1},
{"OwnerGetManyWithAllProperties", "firstname LIKE ?", []interface{}{"%test%"}, []string{}, 3},
{"OwnerGetManyWithOneProperty", "firstname LIKE ?", []interface{}{"%test%"}, []string{"firstname"}, 3},
{"OwnerGetNoneWithAllProperties", "owner_id=?", []interface{}{303}, []string{}, 0},
{"OwnerGetNoneWithOneProperty", "owner_id=?", []interface{}{303}, []string{"firstname"}, 0},
}
}
For the stable repo on the other hand we have these “GetMany” test cases:
func (st *stableTest) createGetManyTestCases() []getManyTestCase {
return []getManyTestCase{
{"StableGetOneWithAllProperties", "stable_id=?", []interface{}{1}, []string{}, 1},
{"StableGetOneWithOneProperty", "stable_id=?", []interface{}{1}, []string{"stable_id"}, 1},
{"StableGetNone", "name LIKE ?", []interface{}{"%elephant%"}, []string{}, 0},
}
}
The prep
step creates a test table in the test db:
func (ot *ownerTest) prep() error {
cols := []string{
"owner_id INT NOT NULL AUTO_INCREMENT",
"user_id VARCHAR(20)",
"firstname VARCHAR(50) NOT NULL",
"lastname VARCHAR(50)",
}
err := ot.db.CreateTable(ot.tablename, ot.idKey, cols)
return err
}
As mentioned, for some repos the generic tests that are run for all aren’t enough, which is why there is a method to get and run additional tests:
func (ot *ownerTest) getAdditionalTests() []func(t *testing.T) {
tests := []func(t *testing.T){
ot.ownerFindStablesTest,
}
return tests
}
This test is not complete and only has one test case, but you get the idea:
func (ot *ownerTest) ownerFindStablesTest(t *testing.T) {
t.Run("OwnerFindStablesTest", func(t *testing.T) {
// Prep step
orepo := ot.createRepo(ot.db)
owner := domain.Owner{
UserId: "0",
FirstName: "Bob",
LastName: "Smith",
}
ownerid, err := orepo.InsertOne(&owner)
if err != nil {
t.Fatalf("Could not create owner: %s", err)
}
stable := domain.Stable{
OwnerId: int(ownerid.(int64)),
Name: "Test Stable Name",
}
srepo := domain.NewStableRepo(database)
_, err = srepo.InsertOne(&stable)
if err != nil {
t.Fatalf("Could not create stable: %s", err)
}
// Retrieve
owners, err := orepo.GetMany("owner_id = ?", []interface{}{ownerid}, []string{"owner_id", "firstname"})
if err != nil {
t.Fatalf("Could not retrieve owner: %s", err)
}
if len(owners) == 0 {
t.Fatalf("Could not find owner with id %v", ownerid)
}
for _, o := range owners {
ownr := o.(domain.Owner)
stables := ownr.GetStables()
if len(stables) < 1 {
t.Fatalf("Could not find expected stable!")
}
}
})
}
The main test file
The actual tests run within repo_test.go
. It, too, does some preparation before running any tests. In TestsMain
I create the test database and the owner and stable tests, run the tests, and then clean up:
func TestMain(m *testing.M) {
sqlxDb, db := tests.CreateTestDB(testDbName)
database = db
rtests := []repoTest{
newOwnerTest(database),
newStableTest(database),
}
for _, test := range rtests {
err := test.prep()
if err == nil {
repoTests = append(repoTests, test)
} else {
fmt.Printf("\nError when prepping test: %s\n", err.Error())
tests.DeleteTestDB(sqlxDb, testDbName)
return
}
}
fmt.Printf("\nCount of repo tests to run: %d\n", len(repoTests))
ret := m.Run()
fmt.Println("Deleting test DB")
tests.DeleteTestDB(sqlxDb, testDbName)
os.Exit(ret)
}
Here is the TestGetMany
test, the test cases for which we get out of the owner and repo tests. As you can see I loop through the repoTests
we created, get the relevant test cases, and then run though each of them.
func TestGetMany(t *testing.T) {
for _, test := range repoTests {
testCases := test.createGetManyTestCases()
for _, tc := range testCases {
fmt.Printf("Running TestGetMany Test Case %s.", tc.tcname)
t.Run(fmt.Sprintf("%s, want %d results", tc.tcname, tc.want), func(t *testing.T) {
repo := test.createRepo(database)
models, err := repo.GetMany(tc.where, tc.params, tc.fields)
if err != nil {
t.Errorf("Test case %s failed: %v", tc.tcname, err)
return
}
if len(models) != tc.want {
t.Errorf("Test case %s got %d results; expected %d", tc.tcname, len(models), tc.want)
} else {
fmt.Println(tc.tcname + " Success")
}
})
}
}
}
And of course we need to run any additional tests we may or may not have defined in each repoTest
:
func TestAdditionalTests(t *testing.T) {
for _, test := range repoTests {
testCases := test.getAdditionalTests()
for _, tc := range testCases {
tc(t)
}
}
}
That is pretty much it…I’m going to work on test isolation next.