Skip to content

Commit

Permalink
chore(allsrv): add allsrvc CLI companion
Browse files Browse the repository at this point in the history
Again here we start with a little refactoring. This time we're creating
a new `allsrvtesting` pkg to hold the reusable code bits. Now we can
implement our CLI and then create the svc implemenation utilizing the cli
in our tests. With a handful of lines of code we're able to create a
high degree of certainty in the implementation of our CLI. The
implementation and testing both, are not limited by the SVC behavior.
You can extend the CLI with additional behavior beyond that of the SVC.
Additional tests can be added to accomodate any additional behavior.

Here we are at the end of the session where we've matured an intensely
immature service implementation. We've covered a lot of ground. We can
sum it up quickly with:

  1. Tests provide certainty we've not broken the existing implementation
  2. Versioning an API helps us transition into the replacement
      * note: we determined in that the v1 was not serving our users
              best interest, so we moved onto a structured JSON-API spec
  3. Observability is SUUUPER important. Just like with testing, we want
     to keep a close eye on our metrics. We want to make sure our changes
     are improving the bottom line. Without this information, we're flying
     blind.
  4. Isolating the SVC/usecases from the transport & db layers gives us
     freedom to reuse that business logic across any number of transport
     & db layers, improve our observability stack along the way, and allow
     us to create a reusable test suite that is usable across any
     implementation of the SVC. With that test suite, creating and
     verifying a new SVC implementation.
  • Loading branch information
jsteenb2 committed Jul 10, 2024
1 parent 808eb0f commit 32a17c0
Show file tree
Hide file tree
Showing 11 changed files with 391 additions and 140 deletions.
34 changes: 34 additions & 0 deletions allsrv/allsrvtesting/service_inmem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package allsrvtesting

import (
"log/slog"
"testing"

"github.com/hashicorp/go-metrics"

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

func NewInmemSVC(t *testing.T, opts SVCTestOpts) allsrv.SVC {
db := new(allsrv.InmemDB)
opts.PrepDB(t, db)

var svc allsrv.SVC = allsrv.NewService(db, opts.SVCOpts...)
svc = allsrv.SVCLogging(newTestLogger(t))(svc)
svc = allsrv.ObserveSVC(metrics.Default())(svc)

return svc
}

func newTestLogger(t *testing.T) *slog.Logger {
return slog.New(slog.NewJSONHandler(&testr{t: t}, nil))
}

type testr struct {
t *testing.T
}

func (t *testr) Write(p []byte) (n int, err error) {
t.t.Log(string(p))
return len(p), nil
}
125 changes: 61 additions & 64 deletions allsrv/svc_suite_test.go → allsrv/allsrvtesting/test_suite.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package allsrv_test
package allsrvtesting

import (
"context"
"log/slog"
"testing"
"time"

Expand All @@ -15,22 +14,22 @@ import (
var start = time.Time{}.Add(time.Hour).UTC()

type (
svcInitFn func(t *testing.T, opts svcTestOpts) svcDeps
SVCInitFn func(t *testing.T, opts SVCTestOpts) SVCDeps

svcDeps struct {
svc allsrv.SVC
SVCDeps struct {
SVC allsrv.SVC
}

svcTestOpts struct {
prepDB func(t *testing.T, db allsrv.DB)
svcOpts []func(svc *allsrv.Service)
SVCTestOpts struct {
PrepDB func(t *testing.T, db allsrv.DB)
SVCOpts []func(svc *allsrv.Service)
}
)

func testSVC(t *testing.T, initFn svcInitFn) {
func TestSVC(t *testing.T, initFn SVCInitFn) {
tests := []struct {
name string
testFn func(t *testing.T, initFn svcInitFn)
testFn func(t *testing.T, initFn SVCInitFn)
}{
{name: "Create", testFn: testSVCCreate},
{name: "Read", testFn: testSVCRead},
Expand All @@ -44,7 +43,7 @@ func testSVC(t *testing.T, initFn svcInitFn) {
}
}

func testSVCCreate(t *testing.T, initFn svcInitFn) {
func testSVCCreate(t *testing.T, initFn SVCInitFn) {
type (
inputs struct {
foo allsrv.Foo
Expand All @@ -55,7 +54,7 @@ func testSVCCreate(t *testing.T, initFn svcInitFn) {

tests := []struct {
name string
opts svcTestOpts
opts SVCTestOpts
input inputs
want wantFn
}{
Expand Down Expand Up @@ -97,8 +96,8 @@ func testSVCCreate(t *testing.T, initFn svcInitFn) {
},
{
name: "with foo with conflicting name should fail",
opts: svcTestOpts{
prepDB: createFoos(allsrv.Foo{ID: "9000", Name: "existing-foo"}),
opts: SVCTestOpts{
PrepDB: CreateFoos(allsrv.Foo{ID: "9000", Name: "existing-foo"}),
},
input: inputs{
foo: allsrv.Foo{
Expand Down Expand Up @@ -131,15 +130,15 @@ func testSVCCreate(t *testing.T, initFn svcInitFn) {
deps := initFn(t, withTestOptions(tt.opts))

// action
got, err := deps.svc.CreateFoo(context.TODO(), tt.input.foo)
got, err := deps.SVC.CreateFoo(context.TODO(), tt.input.foo)

// assert
tt.want(t, got, err)
})
}
}

func testSVCRead(t *testing.T, initFn svcInitFn) {
func testSVCRead(t *testing.T, initFn SVCInitFn) {
type (
inputs struct {
id string
Expand Down Expand Up @@ -168,14 +167,14 @@ func testSVCRead(t *testing.T, initFn svcInitFn) {

tests := []struct {
name string
options svcTestOpts
options SVCTestOpts
input inputs
want wantFn
}{
{
name: "with existing id should pass",
options: svcTestOpts{
prepDB: createFoos(ninekFoo, fooTwo),
options: SVCTestOpts{
PrepDB: CreateFoos(ninekFoo, fooTwo),
},
input: inputs{
id: ninekFoo.ID,
Expand All @@ -186,8 +185,8 @@ func testSVCRead(t *testing.T, initFn svcInitFn) {
},
{
name: "with another existing id should pass",
options: svcTestOpts{
prepDB: createFoos(ninekFoo, fooTwo),
options: SVCTestOpts{
PrepDB: CreateFoos(ninekFoo, fooTwo),
},
input: inputs{
id: fooTwo.ID,
Expand Down Expand Up @@ -224,15 +223,15 @@ func testSVCRead(t *testing.T, initFn svcInitFn) {
deps := initFn(t, withTestOptions(tt.options))

// action
got, err := deps.svc.ReadFoo(context.TODO(), tt.input.id)
got, err := deps.SVC.ReadFoo(context.TODO(), tt.input.id)

// assert
tt.want(t, got, err)
})
}
}

func testSVCUpdate(t *testing.T, initFn svcInitFn) {
func testSVCUpdate(t *testing.T, initFn SVCInitFn) {
type (
inputs struct {
upd allsrv.FooUpd
Expand All @@ -243,14 +242,14 @@ func testSVCUpdate(t *testing.T, initFn svcInitFn) {

tests := []struct {
name string
opts svcTestOpts
opts SVCTestOpts
input inputs
want wantFn
}{
{
name: "with valid full update of existing foo should pass",
opts: svcTestOpts{
prepDB: createFoos(allsrv.Foo{
opts: SVCTestOpts{
PrepDB: CreateFoos(allsrv.Foo{
ID: "1",
Name: "first_foo",
Note: "first note",
Expand All @@ -261,8 +260,8 @@ func testSVCUpdate(t *testing.T, initFn svcInitFn) {
input: inputs{
upd: allsrv.FooUpd{
ID: "1",
Name: ptr("updated_foo"),
Note: ptr("updated note"),
Name: Ptr("updated_foo"),
Note: Ptr("updated note"),
},
},
want: func(t *testing.T, updatedFoo allsrv.Foo, updErr error) {
Expand All @@ -277,8 +276,8 @@ func testSVCUpdate(t *testing.T, initFn svcInitFn) {
},
{
name: "with valid name only update of existing foo should pass",
opts: svcTestOpts{
prepDB: createFoos(allsrv.Foo{
opts: SVCTestOpts{
PrepDB: CreateFoos(allsrv.Foo{
ID: "1",
Name: "first_foo",
Note: "first note",
Expand All @@ -289,7 +288,7 @@ func testSVCUpdate(t *testing.T, initFn svcInitFn) {
input: inputs{
upd: allsrv.FooUpd{
ID: "1",
Name: ptr("updated_foo"),
Name: Ptr("updated_foo"),
},
},
want: func(t *testing.T, updatedFoo allsrv.Foo, updErr error) {
Expand All @@ -304,8 +303,8 @@ func testSVCUpdate(t *testing.T, initFn svcInitFn) {
},
{
name: "with valid note only update of existing foo should pass",
opts: svcTestOpts{
prepDB: createFoos(allsrv.Foo{
opts: SVCTestOpts{
PrepDB: CreateFoos(allsrv.Foo{
ID: "1",
Name: "first_foo",
Note: "first note",
Expand All @@ -316,7 +315,7 @@ func testSVCUpdate(t *testing.T, initFn svcInitFn) {
input: inputs{
upd: allsrv.FooUpd{
ID: "1",
Note: ptr("updated note"),
Note: Ptr("updated note"),
},
},
want: func(t *testing.T, updatedFoo allsrv.Foo, updErr error) {
Expand All @@ -334,7 +333,7 @@ func testSVCUpdate(t *testing.T, initFn svcInitFn) {
input: inputs{
upd: allsrv.FooUpd{
ID: "1",
Note: ptr("updated note"),
Note: Ptr("updated note"),
},
},
want: func(t *testing.T, updatedFoo allsrv.Foo, updErr error) {
Expand All @@ -344,14 +343,14 @@ func testSVCUpdate(t *testing.T, initFn svcInitFn) {
},
{
name: "when updating foo too a name that collides with existing should fail",
opts: svcTestOpts{
prepDB: createFoos(allsrv.Foo{ID: "1", Name: "start-foo"}, allsrv.Foo{ID: "9000", Name: "existing-foo"}),
opts: SVCTestOpts{
PrepDB: CreateFoos(allsrv.Foo{ID: "1", Name: "start-foo"}, allsrv.Foo{ID: "9000", Name: "existing-foo"}),
},
input: inputs{
upd: allsrv.FooUpd{
ID: "1",
Name: ptr("existing-foo"),
Note: ptr("some note"),
Name: Ptr("existing-foo"),
Note: Ptr("some note"),
},
},
want: func(t *testing.T, updatedFoo allsrv.Foo, updErr error) {
Expand All @@ -366,15 +365,15 @@ func testSVCUpdate(t *testing.T, initFn svcInitFn) {
deps := initFn(t, withTestOptions(tt.opts))

// action
got, err := deps.svc.UpdateFoo(context.TODO(), tt.input.upd)
got, err := deps.SVC.UpdateFoo(context.TODO(), tt.input.upd)

// assert
tt.want(t, got, err)
})
}
}

func testSVCDel(t *testing.T, initFn svcInitFn) {
func testSVCDel(t *testing.T, initFn SVCInitFn) {
type (
inputs struct {
id string
Expand All @@ -385,14 +384,14 @@ func testSVCDel(t *testing.T, initFn svcInitFn) {

tests := []struct {
name string
options svcTestOpts
options SVCTestOpts
input inputs
want wantFn
}{
{
name: "with id for existing foo should pass",
options: svcTestOpts{
prepDB: createFoos(allsrv.Foo{ID: "9000", Name: "goku"}),
options: SVCTestOpts{
PrepDB: CreateFoos(allsrv.Foo{ID: "9000", Name: "goku"}),
},
input: inputs{
id: "9000",
Expand Down Expand Up @@ -433,30 +432,30 @@ func testSVCDel(t *testing.T, initFn svcInitFn) {
deps := initFn(t, withTestOptions(tt.options))

// action
err := deps.svc.DelFoo(context.TODO(), tt.input.id)
err := deps.SVC.DelFoo(context.TODO(), tt.input.id)

// assert
tt.want(t, deps.svc, err)
tt.want(t, deps.SVC, err)
})
}
}

// withTestOptions provides some sane default values for tests.
func withTestOptions(opts svcTestOpts) svcTestOpts {
if opts.prepDB == nil {
opts.prepDB = func(t *testing.T, db allsrv.DB) {}
func withTestOptions(opts SVCTestOpts) SVCTestOpts {
if opts.PrepDB == nil {
opts.PrepDB = func(t *testing.T, db allsrv.DB) {}
}
// purposefully checking nil here, empty slice indicates no options
if opts.svcOpts == nil {
opts.svcOpts = defaultSVCOpts(start)
if opts.SVCOpts == nil {
opts.SVCOpts = DefaultSVCOpts(start)
}
return opts
}

func defaultSVCOpts(start time.Time) []func(*allsrv.Service) {
func DefaultSVCOpts(start time.Time) []func(*allsrv.Service) {
return []func(*allsrv.Service){
allsrv.WithSVCIDFn(newIDGen(1, 1)),
allsrv.WithSVCNowFn(nowFn(start, time.Hour)),
allsrv.WithSVCIDFn(IDGen(1, 1)),
allsrv.WithSVCNowFn(NowFn(start, time.Hour)),
}
}

Expand All @@ -469,15 +468,13 @@ func wantFoo(want allsrv.Foo) func(t *testing.T, got allsrv.Foo, opErr error) {
}
}

func newTestLogger(t *testing.T) *slog.Logger {
return slog.New(slog.NewJSONHandler(&testr{t: t}, nil))
}

type testr struct {
t *testing.T
}
func CreateFoos(foos ...allsrv.Foo) func(t *testing.T, db allsrv.DB) {
return func(t *testing.T, db allsrv.DB) {
t.Helper()

func (t *testr) Write(p []byte) (n int, err error) {
t.t.Log(string(p))
return len(p), nil
for _, f := range foos {
err := db.CreateFoo(context.TODO(), f)
require.NoError(t, err)
}
}
}
26 changes: 26 additions & 0 deletions allsrv/allsrvtesting/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package allsrvtesting

import (
"strconv"
"time"
)

func IDGen(start, incr int) func() string {
return func() string {
id := strconv.Itoa(start)
start += incr
return id
}
}

func NowFn(start time.Time, incr time.Duration) func() time.Time {
return func() time.Time {
t := start
start = start.Add(incr)
return t
}
}

func Ptr[T any](v T) *T {
return &v
}
Loading

0 comments on commit 32a17c0

Please sign in to comment.