Skip to content

Commit

Permalink
chore(allsrv): add db test suite
Browse files Browse the repository at this point in the history
By adding a DB test suite, we've effectively lowered the bar to entry for
any additional DBs we wish to add. With this test suite, we can re-run it
against a SQL DB, nosql db, etc. and expect it to satisfy the behavior the
tests cover. Interestingly, this is not limited to dbs. There are a number
of great examples of test suites in open source software. Here are a couple
examples:

  * [influxdb](https://github.com/influxdata/influxdb/tree/v2.0.0/testing):
              things to note here are the abstraction around KV and the
              service behavior. However, reusing the pkg name, testing,
              is not something i'd advise doing. It will create confusion.
  * [vice](https://github.com/matryer/vice/blob/master/vicetest/test.go):
          this lib does a great job abstracting over the expected _behavior_
          similar to influxdb. When you look through the test for each queue
          you should see the `vicetest` pkg's exported tests being called.
          The pkg name, vicetest, is very explicit, would highly recommend
          using a similar naming strategy.

The key thing here is the language of the database interface. Just as
the two examples above are abstracting over the behavior, we do the same
here. Since the closest thing we have to a domain type is the `Foo` type,
we utilize that as our domain language. Any db will need to be able take
a domain Foo and persist it in using whatever implementation they desire. We aren't
bleeding any details beyond the point of implementation. To illustrate this,
I removed the GORM struct tags, as the domain type should not be limited
by the db design.

Now that we have a little test suite stood up, go on and add a new db implementation
and make sure it passes the test suite. I will be adding a sql store, with
sqlite, as a example to build from, however, feel free to explore this problem
space however you'd like.

Refs: [InfluxDB Testing](https://github.com/influxdata/influxdb/tree/v2.0.0/testing)
Refs: [Vice Test Suite](https://github.com/matryer/vice/blob/master/vicetest/test.go)
  • Loading branch information
jsteenb2 committed Jul 10, 2024
1 parent 899f636 commit 3cde05e
Show file tree
Hide file tree
Showing 3 changed files with 537 additions and 267 deletions.
264 changes: 2 additions & 262 deletions allsrv/db_inmem_test.go
Original file line number Diff line number Diff line change
@@ -1,273 +1,13 @@
package allsrv_test

import (
"context"
"errors"
"sync"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/jsteenb2/mess/allsrv"
)

func TestInmemDB(t *testing.T) {
t.Run("create foo", func(t *testing.T) {
t.Run("with valid foo should pass", func(t *testing.T) {
db := new(allsrv.InmemDB)

want := allsrv.Foo{
ID: "1",
Name: "name",
Note: "note",
}
err := db.CreateFoo(context.TODO(), want)
require.NoError(t, err)

got, err := db.ReadFoo(context.TODO(), "1")
require.NoError(t, err)

assert.Equal(t, want, got)
})

t.Run("with concurrent valid foo creates should pass", func(t *testing.T) {
db := new(allsrv.InmemDB)

newFoo := func(id string) allsrv.Foo {
return allsrv.Foo{
ID: id,
Name: "name-" + id,
Note: "note-" + id,
}
}

var wg sync.WaitGroup
for _, f := range []allsrv.Foo{newFoo("1"), newFoo("2"), newFoo("3"), newFoo("4"), newFoo("5")} {
wg.Add(1)
go func(f allsrv.Foo) {
defer wg.Done()
require.NoError(t, db.CreateFoo(context.TODO(), f))
}(f)
}
wg.Wait()
})

t.Run("with foo containing name that already exists should fail", func(t *testing.T) {
db := new(allsrv.InmemDB)

want := allsrv.Foo{
ID: "1",
Name: "collision",
Note: "note",
}
err := db.CreateFoo(context.TODO(), want)
require.NoError(t, err)

err = db.CreateFoo(context.TODO(), want)

// this is pretty gross, we're matching against a raw error/text value
// any change in the error message means we have to update tests too
wantErr := errors.New("foo collision exists")
assert.Equal(t, wantErr.Error(), err.Error())
})
})

t.Run("read foo", func(t *testing.T) {
t.Run("with id for existing foo should pass", func(t *testing.T) {
db := new(allsrv.InmemDB)

want := allsrv.Foo{
ID: "1",
Name: "name",
Note: "note",
}
err := db.CreateFoo(context.TODO(), want)
require.NoError(t, err)

got, err := db.ReadFoo(context.TODO(), "1")
require.NoError(t, err)

assert.Equal(t, want, got)
})

t.Run("with concurrent valid foo update the reading should pass", func(t *testing.T) {
db := new(allsrv.InmemDB)
require.NoError(t, db.CreateFoo(context.TODO(), allsrv.Foo{
ID: "1",
Name: "one",
Note: "note",
}))

newFoo := func(note string) allsrv.Foo {
return allsrv.Foo{
ID: "1",
Name: "one",
Note: note,
}
}

var wg sync.WaitGroup
for _, f := range []allsrv.Foo{newFoo("a"), newFoo("b"), newFoo("c"), newFoo("d"), newFoo("e")} {
wg.Add(1)
go func(f allsrv.Foo) {
defer wg.Done()
require.NoError(t, db.UpdateFoo(context.TODO(), f))
}(f)
}

got, err := db.ReadFoo(context.TODO(), "1")
require.NoError(t, err)

assert.Contains(t, []string{"note", "a", "b", "c", "d", "e"}, got.Note)
wg.Wait()
})

t.Run("with id for non-existent foo should fail", func(t *testing.T) {
db := new(allsrv.InmemDB)

_, err := db.ReadFoo(context.TODO(), "1")

// this is pretty gross, we're matching against a raw error/text value
// any change in the error message means we have to update tests too
want := errors.New("foo not found for id: 1")
assert.Equal(t, want.Error(), err.Error())
})
})

t.Run("update foo", func(t *testing.T) {
t.Run("with valid update for existing foo should pass", func(t *testing.T) {
db := new(allsrv.InmemDB)

want := allsrv.Foo{
ID: "1",
Name: "name",
Note: "note",
}
err := db.CreateFoo(context.TODO(), want)
require.NoError(t, err)

want.Note = "some other note"
err = db.UpdateFoo(context.TODO(), want)
require.NoError(t, err)

got, err := db.ReadFoo(context.TODO(), "1")
require.NoError(t, err)

assert.Equal(t, want, got)
})

t.Run("with concurrent valid foo updates should pass", func(t *testing.T) {
db := new(allsrv.InmemDB)
require.NoError(t, db.CreateFoo(context.TODO(), allsrv.Foo{
ID: "1",
Name: "one",
Note: "note",
}))

newFoo := func(note string) allsrv.Foo {
return allsrv.Foo{
ID: "1",
Name: "one",
Note: note,
}
}

var wg sync.WaitGroup
for _, f := range []allsrv.Foo{newFoo("a"), newFoo("b"), newFoo("c"), newFoo("d"), newFoo("e")} {
wg.Add(1)
go func(f allsrv.Foo) {
defer wg.Done()
require.NoError(t, db.UpdateFoo(context.TODO(), f))
}(f)
}

got, err := db.ReadFoo(context.TODO(), "1")
require.NoError(t, err)
wg.Wait()

assert.Contains(t, []string{"note", "a", "b", "c", "d", "e"}, got.Note)
})

t.Run("with update for non-existent foo should fail", func(t *testing.T) {
db := new(allsrv.InmemDB)

err := db.UpdateFoo(context.TODO(), allsrv.Foo{
ID: "1",
Name: "name",
Note: "note",
})

// this is pretty gross, we're matching against a raw error/text value
// any change in the error message means we have to update tests too
want := errors.New("foo not found for id: 1")
assert.Equal(t, want.Error(), err.Error())
})
})

t.Run("delete foo", func(t *testing.T) {
t.Run("with id for existing foo should pass", func(t *testing.T) {
db := new(allsrv.InmemDB)

err := db.CreateFoo(context.TODO(), allsrv.Foo{
ID: "1",
Name: "name",
Note: "note",
})
require.NoError(t, err)

err = db.DelFoo(context.TODO(), "1")
require.NoError(t, err)

_, err = db.ReadFoo(context.TODO(), "1")

// this is pretty gross, we're matching against a raw error/text value
// any change in the error message means we have to update tests too
want := errors.New("foo not found for id: 1")
assert.Equal(t, want.Error(), err.Error())
})

t.Run("with concurrent valid foo creates should pass", func(t *testing.T) {
db := new(allsrv.InmemDB)

newFoo := func(id string) allsrv.Foo {
return allsrv.Foo{
ID: id,
Name: "name-" + id,
Note: "note-" + id,
}
}

for _, f := range []allsrv.Foo{newFoo("1"), newFoo("2"), newFoo("3"), newFoo("4"), newFoo("5")} {
require.NoError(t, db.CreateFoo(context.TODO(), f))
}

var wg sync.WaitGroup
for _, id := range []string{"1", "2", "3", "4", "5"} {
wg.Add(1)
go func(id string) {
defer wg.Done()
require.NoError(t, db.DelFoo(context.TODO(), id))
}(id)
}
wg.Wait()

for _, id := range []string{"1", "2", "3", "4", "5"} {
err := db.DelFoo(context.TODO(), id)
wantErr := errors.New("foo not found for id: " + id)
require.Error(t, wantErr, err)
}
})

t.Run("with id for non-existent foo should fail", func(t *testing.T) {
db := new(allsrv.InmemDB)

err := db.DelFoo(context.TODO(), "1")

// this is pretty gross, we're matching against a raw error/text value
// any change in the error message means we have to update tests too
want := errors.New("foo not found for id: 1")
assert.Equal(t, want.Error(), err.Error())
})
testDB(t, func(t *testing.T) allsrv.DB {
return new(allsrv.InmemDB)
})
}
Loading

0 comments on commit 3cde05e

Please sign in to comment.