Testing SnailLife Go repositories

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.

comments powered by Disqus