diff --git a/internal/testtools/assert.go b/internal/testtools/assert.go new file mode 100644 index 0000000..8763f1a --- /dev/null +++ b/internal/testtools/assert.go @@ -0,0 +1,70 @@ +package tt + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// AssertEqual will compare the got argument with the expected argument +// and fail the test with an appropriate error message if they don't match. +func AssertEqual(t *testing.T, got interface{}, expected interface{}, msg ...interface{}) { + require.Equal(t, expected, got, msg...) +} + +// AssertNotEqual will compare the got argument with the expected argument +// and fail the test with an appropriate error message if they match. +func AssertNotEqual(t *testing.T, got interface{}, expected interface{}, msg ...interface{}) { + require.NotEqual(t, expected, got, msg...) +} + +// AssertNoErr will check if the input error is nil, and if not +// it will fail the test with an appropriate error message. +func AssertNoErr(t *testing.T, err error) { + require.Equal(t, nil, err, "received unexpected error: %s", err) +} + +// AssertErrContains will first check if the error that the error +// indeed is not nil, and then check if its error message contains +// all the substrs specified on the substrs argument. +// +// In case either assertion fails it will fail the test with +// an appropriate error message. +func AssertErrContains(t *testing.T, err error, substrs ...string) { + require.NotEqual(t, nil, err, "expected an error but the error is nil") + + msg := err.Error() + + for _, substr := range substrs { + require.True(t, + strings.Contains(msg, substr), + "missing substring '%s' in error message: '%s'", + substr, msg, + ) + } +} + +// AssertApproxDuration checks if the durations v1 and v2 are close up to the tolerance specified. +// The format and args slice can be used for generating an appropriate error message if they are not. +func AssertApproxDuration(t *testing.T, tolerance time.Duration, v1, v2 time.Duration, format string, args ...interface{}) { + diff := v1 - v2 + if diff < 0 { + diff = -diff + } + + require.True(t, diff <= tolerance, fmt.Sprintf(format, args...)) +} + +// AssertApproxTime checks if the times v1 and v2 are close up to the tolerance specified. +// The format and args slice can be used for generating an appropriate error message if they are not. +func AssertApproxTime(t *testing.T, tolerance time.Duration, v1, v2 time.Time, format string, args ...interface{}) { + diff := v1.Sub(v2) + if diff < 0 { + diff = -diff + } + + require.True(t, diff <= tolerance, fmt.Sprintf(format, args...)) +} diff --git a/internal/testtools/panic.go b/internal/testtools/panic.go new file mode 100644 index 0000000..9f0410c --- /dev/null +++ b/internal/testtools/panic.go @@ -0,0 +1,18 @@ +package tt + +// PanicHandler will run the input function and recover +// from any panics it might generate. +// +// It will then save the panic payload and return it +// so it can be asserted by other functions on the test. +func PanicHandler(fn func()) (panicPayload interface{}) { + defer func() { + // Overwrites the panic payload if a pannic actually occurs: + if r := recover(); r != nil { + panicPayload = r + } + }() + + fn() + return nil +} diff --git a/mocks.go b/mocks.go index 3576205..8dd5540 100644 --- a/mocks.go +++ b/mocks.go @@ -81,7 +81,7 @@ func (m Mock) SetFallbackDatabase(db Provider) Mock { // Insert ... func (m Mock) Insert(ctx context.Context, table Table, record interface{}) error { if m.InsertFn == nil { - panic(fmt.Errorf("Mock.Insert(ctx, %v, %v) called but the ksql.Mock.InsertFn() is not set", table, record)) + panic(fmt.Errorf("ksql.Mock.Insert(ctx, %v, %v) called but the ksql.Mock.InsertFn() is not set", table, record)) } return m.InsertFn(ctx, table, record) } @@ -89,7 +89,7 @@ func (m Mock) Insert(ctx context.Context, table Table, record interface{}) error // Update ... func (m Mock) Update(ctx context.Context, table Table, record interface{}) error { if m.UpdateFn == nil { - panic(fmt.Errorf("Mock.Update(ctx, %v, %v) called but the ksql.Mock.UpdateFn() is not set", table, record)) + panic(fmt.Errorf("ksql.Mock.Update(ctx, %v, %v) called but the ksql.Mock.UpdateFn() is not set", table, record)) } return m.UpdateFn(ctx, table, record) } @@ -97,7 +97,7 @@ func (m Mock) Update(ctx context.Context, table Table, record interface{}) error // Delete ... func (m Mock) Delete(ctx context.Context, table Table, idOrRecord interface{}) error { if m.DeleteFn == nil { - panic(fmt.Errorf("Mock.Delete(ctx, %v, %v) called but the ksql.Mock.DeleteFn() is not set", table, idOrRecord)) + panic(fmt.Errorf("ksql.Mock.Delete(ctx, %v, %v) called but the ksql.Mock.DeleteFn() is not set", table, idOrRecord)) } return m.DeleteFn(ctx, table, idOrRecord) } @@ -105,7 +105,7 @@ func (m Mock) Delete(ctx context.Context, table Table, idOrRecord interface{}) e // Query ... func (m Mock) Query(ctx context.Context, records interface{}, query string, params ...interface{}) error { if m.QueryFn == nil { - panic(fmt.Errorf("Mock.Query(ctx, %v, %s, %v) called but the ksql.Mock.QueryFn() is not set", records, query, params)) + panic(fmt.Errorf("ksql.Mock.Query(ctx, %v, %s, %v) called but the ksql.Mock.QueryFn() is not set", records, query, params)) } return m.QueryFn(ctx, records, query, params...) } @@ -113,7 +113,7 @@ func (m Mock) Query(ctx context.Context, records interface{}, query string, para // QueryOne ... func (m Mock) QueryOne(ctx context.Context, record interface{}, query string, params ...interface{}) error { if m.QueryOneFn == nil { - panic(fmt.Errorf("Mock.QueryOne(ctx, %v, %s, %v) called but the ksql.Mock.QueryOneFn() is not set", record, query, params)) + panic(fmt.Errorf("ksql.Mock.QueryOne(ctx, %v, %s, %v) called but the ksql.Mock.QueryOneFn() is not set", record, query, params)) } return m.QueryOneFn(ctx, record, query, params...) } @@ -121,7 +121,7 @@ func (m Mock) QueryOne(ctx context.Context, record interface{}, query string, pa // QueryChunks ... func (m Mock) QueryChunks(ctx context.Context, parser ChunkParser) error { if m.QueryChunksFn == nil { - panic(fmt.Errorf("Mock.QueryChunks(ctx, %v) called but the ksql.Mock.QueryChunksFn() is not set", parser)) + panic(fmt.Errorf("ksql.Mock.QueryChunks(ctx, %v) called but the ksql.Mock.QueryChunksFn() is not set", parser)) } return m.QueryChunksFn(ctx, parser) } @@ -129,7 +129,7 @@ func (m Mock) QueryChunks(ctx context.Context, parser ChunkParser) error { // Exec ... func (m Mock) Exec(ctx context.Context, query string, params ...interface{}) (rowsAffected int64, _ error) { if m.ExecFn == nil { - panic(fmt.Errorf("Mock.Exec(ctx, %s, %v) called but the ksql.Mock.ExecFn() is not set", query, params)) + panic(fmt.Errorf("ksql.Mock.Exec(ctx, %s, %v) called but the ksql.Mock.ExecFn() is not set", query, params)) } return m.ExecFn(ctx, query, params...) } diff --git a/mocks_test.go b/mocks_test.go new file mode 100644 index 0000000..bcfa55e --- /dev/null +++ b/mocks_test.go @@ -0,0 +1,145 @@ +// This test was written mostly for test coverage since the mock is trivial +package ksql_test + +import ( + "context" + "testing" + + "github.com/vingarcia/ksql" + tt "github.com/vingarcia/ksql/internal/testtools" +) + +func TestMock(t *testing.T) { + UsersTable := ksql.NewTable("users", "id") + type User struct { + ID int `ksql:"id"` + Name string `ksql:"name"` + Age int `ksql:"age"` + } + + t.Run("testing unset behaviors for all methods", func(t *testing.T) { + t.Run("Insert should panic", func(t *testing.T) { + ctx := context.Background() + mock := ksql.Mock{} + panicPayload := tt.PanicHandler(func() { + mock.Insert(ctx, UsersTable, &User{ + Name: "fake-name", + Age: 42, + }) + }) + + err, ok := panicPayload.(error) + tt.AssertEqual(t, ok, true) + tt.AssertErrContains(t, err, "ksql.Mock.Insert(", "ksql.Mock.InsertFn", "not set") + }) + + t.Run("Update should panic", func(t *testing.T) { + ctx := context.Background() + mock := ksql.Mock{} + panicPayload := tt.PanicHandler(func() { + mock.Update(ctx, UsersTable, &User{ + ID: 4242, + Name: "fake-name", + Age: 42, + }) + }) + + err, ok := panicPayload.(error) + tt.AssertEqual(t, ok, true) + tt.AssertErrContains(t, err, "ksql.Mock.Update(", "ksql.Mock.UpdateFn", "not set") + }) + + t.Run("Delete should panic", func(t *testing.T) { + ctx := context.Background() + mock := ksql.Mock{} + panicPayload := tt.PanicHandler(func() { + mock.Delete(ctx, UsersTable, &User{ + ID: 4242, + Name: "fake-name", + Age: 42, + }) + }) + + err, ok := panicPayload.(error) + tt.AssertEqual(t, ok, true) + tt.AssertErrContains(t, err, "ksql.Mock.Delete(", "ksql.Mock.DeleteFn", "not set") + }) + + t.Run("Query should panic", func(t *testing.T) { + ctx := context.Background() + mock := ksql.Mock{} + panicPayload := tt.PanicHandler(func() { + var users []User + mock.Query(ctx, &users, "SELECT * FROM user WHERE age = ?", 42) + }) + + err, ok := panicPayload.(error) + tt.AssertEqual(t, ok, true) + tt.AssertErrContains(t, err, "ksql.Mock.Query(", "ksql.Mock.QueryFn", "not set") + }) + + t.Run("QueryOne should panic", func(t *testing.T) { + ctx := context.Background() + mock := ksql.Mock{} + panicPayload := tt.PanicHandler(func() { + var user User + mock.QueryOne(ctx, &user, "SELECT * FROM user WHERE id = ?", 4242) + }) + + err, ok := panicPayload.(error) + tt.AssertEqual(t, ok, true) + tt.AssertErrContains(t, err, "ksql.Mock.QueryOne(", "ksql.Mock.QueryOneFn", "not set") + }) + + t.Run("QueryChunks should panic", func(t *testing.T) { + ctx := context.Background() + mock := ksql.Mock{} + panicPayload := tt.PanicHandler(func() { + var users []User + mock.QueryChunks(ctx, ksql.ChunkParser{ + Query: "SELECT * FROM users WHERE age = ?", + Params: []interface{}{ + 4242, + }, + ChunkSize: 10, + ForEachChunk: func(chunk []User) error { + users = append(users, chunk...) + return nil + }, + }) + }) + + err, ok := panicPayload.(error) + tt.AssertEqual(t, ok, true) + tt.AssertErrContains(t, err, "ksql.Mock.QueryChunks(", "ksql.Mock.QueryChunksFn", "not set") + }) + + t.Run("Exec should panic", func(t *testing.T) { + ctx := context.Background() + mock := ksql.Mock{} + panicPayload := tt.PanicHandler(func() { + mock.Exec(ctx, "INSERT INTO users_permissions(user_id, permission_id) VALUES (?, ?)", 4242, 4) + }) + + err, ok := panicPayload.(error) + tt.AssertEqual(t, ok, true) + tt.AssertErrContains(t, err, "ksql.Mock.Exec(", "ksql.Mock.ExecFn", "not set") + }) + + t.Run("Transaction should not panic", func(t *testing.T) { + ctx := context.Background() + mock := ksql.Mock{} + + executed := false + panicPayload := tt.PanicHandler(func() { + mock.Transaction(ctx, func(db ksql.Provider) error { + executed = true + return nil + }) + }) + + tt.AssertEqual(t, panicPayload, nil) + tt.AssertEqual(t, executed, true) + }) + }) +}