Reorganize files so the test helpers are grouped in the same pkg

pull/2/head
Vinícius Garcia 2021-06-11 12:25:24 -03:00
parent 5b9b0dd00d
commit 20f49eb22b
7 changed files with 172 additions and 164 deletions

View File

@ -469,9 +469,10 @@ that we actually care about, so it's better not to use composite structs.
This library has a few helper functions for helping your tests:
- `ksql.FillStructWith(struct interface{}, dbRow map[string]interface{}) error`
- `ksql.FillSliceWith(structSlice interface{}, dbRows []map[string]interface{}) error`
- `ksql.StructToMap(struct interface{}) (map[string]interface{}, error)`
- `structs.FillStructWith(struct interface{}, dbRow map[string]interface{}) error`
- `structs.FillSliceWith(structSlice interface{}, dbRows []map[string]interface{}) error`
- `structs.StructToMap(struct interface{}) (map[string]interface{}, error)`
- `structs.CallFunctionWithRows(fn interface{}, rows []map[string]interface{}) (map[string]interface{}, error)`
If you want to see examples (we have examples for all the public functions) just
read the example tests available on our [example service](./examples/example_service)

View File

@ -198,7 +198,7 @@ func TestStreamAllUsers(t *testing.T) {
mockDB.EXPECT().QueryChunks(gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, parser ksql.ChunkParser) error {
// Chunk 1:
err := ksql.CallFunctionWithRows(parser.ForEachChunk, []map[string]interface{}{
err := structs.CallFunctionWithRows(parser.ForEachChunk, []map[string]interface{}{
{
"id": 1,
"name": "fake name",
@ -215,7 +215,7 @@ func TestStreamAllUsers(t *testing.T) {
}
// Chunk 2:
err = ksql.CallFunctionWithRows(parser.ForEachChunk, []map[string]interface{}{
err = structs.CallFunctionWithRows(parser.ForEachChunk, []map[string]interface{}{
{
"id": 3,
"name": "yet another fake name",

34
ksql.go
View File

@ -244,7 +244,7 @@ func (c DB) QueryChunks(
parser ChunkParser,
) error {
fnValue := reflect.ValueOf(parser.ForEachChunk)
chunkType, err := parseInputFunc(parser.ForEachChunk)
chunkType, err := structs.ParseInputFunc(parser.ForEachChunk)
if err != nil {
return err
}
@ -759,38 +759,6 @@ func (c DB) Transaction(ctx context.Context, fn func(SQLProvider) error) error {
}
}
var errType = reflect.TypeOf(new(error)).Elem()
func parseInputFunc(fn interface{}) (reflect.Type, error) {
if fn == nil {
return nil, fmt.Errorf("the ForEachChunk attribute is required and cannot be nil")
}
t := reflect.TypeOf(fn)
if t.Kind() != reflect.Func {
return nil, fmt.Errorf("the ForEachChunk callback must be a function")
}
if t.NumIn() != 1 {
return nil, fmt.Errorf("the ForEachChunk callback must have 1 argument")
}
if t.NumOut() != 1 {
return nil, fmt.Errorf("the ForEachChunk callback must have a single return value")
}
if t.Out(0) != errType {
return nil, fmt.Errorf("the return value of the ForEachChunk callback must be of type error")
}
argsType := t.In(0)
if argsType.Kind() != reflect.Slice {
return nil, fmt.Errorf("the argument of the ForEachChunk callback must a slice of structs")
}
return argsType, nil
}
type nopScanner struct{}
var nopScannerValue = reflect.ValueOf(&nopScanner{}).Interface()

40
structs/func_parser.go Normal file
View File

@ -0,0 +1,40 @@
package structs
import (
"fmt"
"reflect"
)
var errType = reflect.TypeOf(new(error)).Elem()
// ParseInputFunc is used exclusively for parsing
// the ForEachChunk function used on the QueryChunks method.
func ParseInputFunc(fn interface{}) (reflect.Type, error) {
if fn == nil {
return nil, fmt.Errorf("the ForEachChunk attribute is required and cannot be nil")
}
t := reflect.TypeOf(fn)
if t.Kind() != reflect.Func {
return nil, fmt.Errorf("the ForEachChunk callback must be a function")
}
if t.NumIn() != 1 {
return nil, fmt.Errorf("the ForEachChunk callback must have 1 argument")
}
if t.NumOut() != 1 {
return nil, fmt.Errorf("the ForEachChunk callback must have a single return value")
}
if t.Out(0) != errType {
return nil, fmt.Errorf("the return value of the ForEachChunk callback must be of type error")
}
argsType := t.In(0)
if argsType.Kind() != reflect.Slice {
return nil, fmt.Errorf("the argument of the ForEachChunk callback must a slice of structs")
}
return argsType, nil
}

View File

@ -4,8 +4,6 @@ import (
"fmt"
"reflect"
"strings"
"github.com/pkg/errors"
)
// StructInfo stores metainformation of the struct
@ -120,56 +118,6 @@ func StructToMap(obj interface{}) (map[string]interface{}, error) {
return m, nil
}
// FillStructWith is meant to be used on unit tests to mock
// the response from the database.
//
// The first argument is any struct you are passing to a ksql func,
// and the second is a map representing a database row you want
// to use to update this struct.
func FillStructWith(record interface{}, dbRow map[string]interface{}) error {
v := reflect.ValueOf(record)
t := v.Type()
if t.Kind() != reflect.Ptr {
return fmt.Errorf(
"FillStructWith: expected input to be a pointer to struct but got %T",
record,
)
}
t = t.Elem()
v = v.Elem()
if t.Kind() != reflect.Struct {
return fmt.Errorf(
"FillStructWith: expected input kind to be a struct but got %T",
record,
)
}
info := getCachedTagInfo(tagInfoCache, t)
for colName, rawSrc := range dbRow {
fieldInfo := info.ByName(colName)
if !fieldInfo.Valid {
// Ignore columns not tagged with `ksql:"..."`
continue
}
src := NewPtrConverter(rawSrc)
dest := v.Field(fieldInfo.Index)
destType := t.Field(fieldInfo.Index).Type
destValue, err := src.Convert(destType)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("FillStructWith: error on field `%s`", colName))
}
dest.Set(destValue)
}
return nil
}
// PtrConverter was created to make it easier
// to handle conversion between ptr and non ptr types, e.g.:
//
@ -251,49 +199,6 @@ func (p PtrConverter) Convert(destType reflect.Type) (reflect.Value, error) {
return destValue, nil
}
// FillSliceWith is meant to be used on unit tests to mock
// the response from the database.
//
// The first argument is any slice of structs you are passing to a ksql func,
// and the second is a slice of maps representing the database rows you want
// to use to update this struct.
func FillSliceWith(entities interface{}, dbRows []map[string]interface{}) error {
sliceRef := reflect.ValueOf(entities)
sliceType := sliceRef.Type()
if sliceType.Kind() != reflect.Ptr {
return fmt.Errorf(
"FillSliceWith: expected input to be a pointer to a slice of structs but got %v",
sliceType,
)
}
structType, isSliceOfPtrs, err := DecodeAsSliceOfStructs(sliceType.Elem())
if err != nil {
return errors.Wrap(err, "FillSliceWith")
}
slice := sliceRef.Elem()
for idx, row := range dbRows {
if slice.Len() <= idx {
var elemValue reflect.Value
elemValue = reflect.New(structType)
if !isSliceOfPtrs {
elemValue = elemValue.Elem()
}
slice = reflect.Append(slice, elemValue)
}
err := FillStructWith(slice.Index(idx).Addr().Interface(), row)
if err != nil {
return errors.Wrap(err, "FillSliceWith")
}
}
sliceRef.Elem().Set(slice)
return nil
}
// This function collects only the names
// that will be used from the input type.
//

125
structs/testhelpers.go Normal file
View File

@ -0,0 +1,125 @@
package structs
import (
"fmt"
"reflect"
"github.com/pkg/errors"
)
// FillStructWith is meant to be used on unit tests to mock
// the response from the database.
//
// The first argument is any struct you are passing to a ksql func,
// and the second is a map representing a database row you want
// to use to update this struct.
func FillStructWith(record interface{}, dbRow map[string]interface{}) error {
v := reflect.ValueOf(record)
t := v.Type()
if t.Kind() != reflect.Ptr {
return fmt.Errorf(
"FillStructWith: expected input to be a pointer to struct but got %T",
record,
)
}
t = t.Elem()
v = v.Elem()
if t.Kind() != reflect.Struct {
return fmt.Errorf(
"FillStructWith: expected input kind to be a struct but got %T",
record,
)
}
info := GetTagInfo(t)
for colName, rawSrc := range dbRow {
fieldInfo := info.ByName(colName)
if !fieldInfo.Valid {
// Ignore columns not tagged with `ksql:"..."`
continue
}
src := NewPtrConverter(rawSrc)
dest := v.Field(fieldInfo.Index)
destType := t.Field(fieldInfo.Index).Type
destValue, err := src.Convert(destType)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("FillStructWith: error on field `%s`", colName))
}
dest.Set(destValue)
}
return nil
}
// FillSliceWith is meant to be used on unit tests to mock
// the response from the database.
//
// The first argument is any slice of structs you are passing to a ksql func,
// and the second is a slice of maps representing the database rows you want
// to use to update this struct.
func FillSliceWith(entities interface{}, dbRows []map[string]interface{}) error {
sliceRef := reflect.ValueOf(entities)
sliceType := sliceRef.Type()
if sliceType.Kind() != reflect.Ptr {
return fmt.Errorf(
"FillSliceWith: expected input to be a pointer to a slice of structs but got %v",
sliceType,
)
}
structType, isSliceOfPtrs, err := DecodeAsSliceOfStructs(sliceType.Elem())
if err != nil {
return errors.Wrap(err, "FillSliceWith")
}
slice := sliceRef.Elem()
for idx, row := range dbRows {
if slice.Len() <= idx {
var elemValue reflect.Value
elemValue = reflect.New(structType)
if !isSliceOfPtrs {
elemValue = elemValue.Elem()
}
slice = reflect.Append(slice, elemValue)
}
err := FillStructWith(slice.Index(idx).Addr().Interface(), row)
if err != nil {
return errors.Wrap(err, "FillSliceWith")
}
}
sliceRef.Elem().Set(slice)
return nil
}
// CallFunctionWithRows was created for helping test the QueryChunks method
func CallFunctionWithRows(fn interface{}, rows []map[string]interface{}) error {
fnValue := reflect.ValueOf(fn)
chunkType, err := ParseInputFunc(fn)
if err != nil {
return err
}
chunk := reflect.MakeSlice(chunkType, 0, len(rows))
// Create a pointer to a slice (required by FillSliceWith)
chunkPtr := reflect.New(chunkType)
chunkPtr.Elem().Set(chunk)
err = FillSliceWith(chunkPtr.Interface(), rows)
if err != nil {
return err
}
err, _ = fnValue.Call([]reflect.Value{chunkPtr.Elem()})[0].Interface().(error)
return err
}

View File

@ -1,31 +0,0 @@
package ksql
import (
"reflect"
"github.com/vingarcia/ksql/structs"
)
// CallFunctionWithRows was created for helping test the QueryChunks method
func CallFunctionWithRows(fn interface{}, rows []map[string]interface{}) error {
fnValue := reflect.ValueOf(fn)
chunkType, err := parseInputFunc(fn)
if err != nil {
return err
}
chunk := reflect.MakeSlice(chunkType, 0, len(rows))
// Create a pointer to a slice (required by FillSliceWith)
chunkPtr := reflect.New(chunkType)
chunkPtr.Elem().Set(chunk)
err = structs.FillSliceWith(chunkPtr.Interface(), rows)
if err != nil {
return err
}
err, _ = fnValue.Call([]reflect.Value{chunkPtr.Elem()})[0].Interface().(error)
return err
}