From 891cd03b5abc1a5af637499cc77301c118c0e6a1 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 26 Sep 2025 16:16:53 -0400 Subject: [PATCH 1/8] adding support for returning more than one mock row --- lazy.go | 52 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/lazy.go b/lazy.go index 5afefe8..aa2e780 100644 --- a/lazy.go +++ b/lazy.go @@ -14,39 +14,47 @@ import ( type MockResults struct { Query string Columns []string - Rows []driver.Value + Rows [][]driver.Value } -func GenerateRandomResults(query string, exampleObj any, keyVal any) (*MockResults, error) { +func GenerateRandomResults(query string, exampleObj any, keyVal any, rowCount int) (*MockResults, error) { if exampleObj == nil { return nil, errors.New("exampleObj cannot be nil") } + if rowCount == 0 { + rowCount = 1 + } retType := reflect.TypeOf(exampleObj) maxFieldCount := retType.NumField() columns := make([]string, 0, maxFieldCount) - rows := make([]driver.Value, 0, maxFieldCount) + rows := make([][]driver.Value, 0, maxFieldCount) - for x := 0; x < maxFieldCount; x++ { - field := retType.Field(x) - dbTag := field.Tag.Get("db") - if dbTag == "" { - continue + //double loop here to allow for multiple rows + for y := 0; y < rowCount; y++ { + for x := 0; x < maxFieldCount; x++ { + field := retType.Field(x) + dbTag := field.Tag.Get("db") + if dbTag == "" { + continue + } + + if y == 0 { + columns = append(columns, dbTag) + } + + if field.Tag.Get("test") == "key" { + rows[y] = append(rows[y], keyVal) + continue + } + + nv := kindToRandom(field) + if nv == nil { + return nil, fmt.Errorf("could not match type: %s", retType.Name()) + } + + rows[y] = append(rows[y], nv) } - - columns = append(columns, dbTag) - - if field.Tag.Get("test") == "key" { - rows = append(rows, keyVal) - continue - } - - nv := kindToRandom(field) - if nv == nil { - return nil, fmt.Errorf("could not match type: %s", retType.Name()) - } - - rows = append(rows, nv) } return &MockResults{ -- 2.49.1 From 428381545db53cb4a0a95d600d14ea4f20f54761 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 26 Sep 2025 16:18:27 -0400 Subject: [PATCH 2/8] adding proper allocations --- lazy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lazy.go b/lazy.go index aa2e780..63230fc 100644 --- a/lazy.go +++ b/lazy.go @@ -28,10 +28,10 @@ func GenerateRandomResults(query string, exampleObj any, keyVal any, rowCount in retType := reflect.TypeOf(exampleObj) maxFieldCount := retType.NumField() columns := make([]string, 0, maxFieldCount) - rows := make([][]driver.Value, 0, maxFieldCount) + rows := make([][]driver.Value, 0) - //double loop here to allow for multiple rows for y := 0; y < rowCount; y++ { + rows[y] = make([]driver.Value, 0) for x := 0; x < maxFieldCount; x++ { field := retType.Field(x) dbTag := field.Tag.Get("db") -- 2.49.1 From 75f6ef8b5b3208d52f36e8f483d38198bf14886d Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 26 Sep 2025 16:25:46 -0400 Subject: [PATCH 3/8] moving to append --- lazy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lazy.go b/lazy.go index 63230fc..f70676c 100644 --- a/lazy.go +++ b/lazy.go @@ -31,7 +31,7 @@ func GenerateRandomResults(query string, exampleObj any, keyVal any, rowCount in rows := make([][]driver.Value, 0) for y := 0; y < rowCount; y++ { - rows[y] = make([]driver.Value, 0) + rows = append(rows, make([]driver.Value, 0)) for x := 0; x < maxFieldCount; x++ { field := retType.Field(x) dbTag := field.Tag.Get("db") -- 2.49.1 From 92b673cb46871137956cc1c3c75c713749abf345 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 26 Sep 2025 16:29:36 -0400 Subject: [PATCH 4/8] persisting primary keys through rows --- lazy.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lazy.go b/lazy.go index f70676c..79cb897 100644 --- a/lazy.go +++ b/lazy.go @@ -17,13 +17,18 @@ type MockResults struct { Rows [][]driver.Value } -func GenerateRandomResults(query string, exampleObj any, keyVal any, rowCount int) (*MockResults, error) { +func GenerateRandomResults(query string, exampleObj any, keyVal []any, rowCount int) (*MockResults, error) { if exampleObj == nil { return nil, errors.New("exampleObj cannot be nil") } if rowCount == 0 { rowCount = 1 } + if len(keyVal) != 0 { + if len(keyVal) != rowCount { + return nil, errors.New("you must provide a key for each row") + } + } retType := reflect.TypeOf(exampleObj) maxFieldCount := retType.NumField() -- 2.49.1 From b9a8c5cd474eddbfc4f0b0537004eb92bf1bdfa3 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 26 Sep 2025 16:33:24 -0400 Subject: [PATCH 5/8] slice indexing --- lazy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lazy.go b/lazy.go index 79cb897..b4ed27a 100644 --- a/lazy.go +++ b/lazy.go @@ -49,7 +49,7 @@ func GenerateRandomResults(query string, exampleObj any, keyVal []any, rowCount } if field.Tag.Get("test") == "key" { - rows[y] = append(rows[y], keyVal) + rows[y] = append(rows[y], keyVal[y]) continue } -- 2.49.1 From 4573594c85ba19703f62fa8ffad6c38b950d22d4 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 26 Sep 2025 16:46:40 -0400 Subject: [PATCH 6/8] moving to struct as param, readme updates --- README.md | 12 +++++++----- lazy.go | 42 +++++++++++++++++++++++++++++------------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index afcdf41..5ff9f23 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,12 @@ This function expects the SQL query, the example response object, and an optiona Example Object The example object argument must be a struct and requires "db" tags. The db tags are then parsed and used to calculate the psuedo-random rows and values -Primary Key The optional primary key argument is used to hardcode a primary key field in the returned mocks, the primary key field in the example struct must have a test tag with the value "key" +Primary Key The optional primary key argument is used to hardcode a primary key field in the returned mocks, the primary key field in the example struct must have a lazy tag with the value "key" + +Row Count Row count sets the amount of mock rows to generate, if set to 0 then one row will be generated. If primary keys are provided you must provide one per row. ```go type Mock struct { - Field1 string `db:"field1" test:"key"` + Field1 string `db:"field1" lazy:"key"` } ``` @@ -22,17 +24,17 @@ func Test() { testArg := 123 query := `SELECT ...` type results struct { - Field1 string `db:"field1" test:"key"` + Field1 string `db:"field1" lazy:"key"` Field2 int `db:"field2"` } - rando, err := GenerateRandomResults(query, results{}, testArg) + rando, err := GenerateRandomResults(query, results{}, []any{testArg}, 1) if err != nil { panic(err) } //the rando object will now have psuedo random values in all fields except for the field containing the test tag set to "key" that field will be hardcoded with the testArg to allow for unit tests to ensure the requested ID flows through - rows := sqlmock.NewRows(rando.Columns).AddRow(rando.Rows...) + rows := sqlmock.NewRows(rando.Columns).AddRows(rando.Rows...) mock.ExpectQuery(rando.Query).WithArgs(testArg).WillReturnRows(rows) ... diff --git a/lazy.go b/lazy.go index b4ed27a..9195290 100644 --- a/lazy.go +++ b/lazy.go @@ -11,35 +11,51 @@ import ( "github.com/jmoiron/sqlx" ) +const ( + DB_TAG = "db" + LAZY_TAG = "lazy" + KEY_VALUE = "key" +) + type MockResults struct { Query string Columns []string Rows [][]driver.Value } -func GenerateRandomResults(query string, exampleObj any, keyVal []any, rowCount int) (*MockResults, error) { - if exampleObj == nil { - return nil, errors.New("exampleObj cannot be nil") +type MockDetails struct { + Query string + Example any + Keys []any + RowCount int +} + +func GenerateRandomResults(m MockDetails) (*MockResults, error) { + if m.Example == nil { + return nil, errors.New("example value cannot be nil") } - if rowCount == 0 { - rowCount = 1 + if reflect.ValueOf(m.Example).Kind() != reflect.Struct { + return nil, errors.New("example value must be a struct") } - if len(keyVal) != 0 { - if len(keyVal) != rowCount { + if m.RowCount == 0 { + m.RowCount = 1 + } + if len(m.Keys) != 0 { + if len(m.Keys) != m.RowCount { return nil, errors.New("you must provide a key for each row") } } - retType := reflect.TypeOf(exampleObj) + retType := reflect.TypeOf(m.Example) maxFieldCount := retType.NumField() columns := make([]string, 0, maxFieldCount) rows := make([][]driver.Value, 0) - for y := 0; y < rowCount; y++ { + for y := 0; y < m.RowCount; y++ { rows = append(rows, make([]driver.Value, 0)) for x := 0; x < maxFieldCount; x++ { field := retType.Field(x) - dbTag := field.Tag.Get("db") + dbTag := field.Tag.Get(DB_TAG) if dbTag == "" { continue } @@ -48,8 +64,8 @@ func GenerateRandomResults(query string, exampleObj any, keyVal []any, rowCount columns = append(columns, dbTag) } - if field.Tag.Get("test") == "key" { - rows[y] = append(rows[y], keyVal[y]) + if field.Tag.Get(LAZY_TAG) == KEY_VALUE { + rows[y] = append(rows[y], m.Keys[y]) continue } @@ -63,7 +79,7 @@ func GenerateRandomResults(query string, exampleObj any, keyVal []any, rowCount } return &MockResults{ - Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(query)), + Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(m.Query)), Columns: columns, Rows: rows, }, nil -- 2.49.1 From 4febf3d87747bb882f4e296481ef0df0141e2736 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 26 Sep 2025 16:48:53 -0400 Subject: [PATCH 7/8] struct rename --- lazy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lazy.go b/lazy.go index 9195290..51fa3f9 100644 --- a/lazy.go +++ b/lazy.go @@ -23,14 +23,14 @@ type MockResults struct { Rows [][]driver.Value } -type MockDetails struct { +type Config struct { Query string Example any Keys []any RowCount int } -func GenerateRandomResults(m MockDetails) (*MockResults, error) { +func GenerateRandomResults(m Config) (*MockResults, error) { if m.Example == nil { return nil, errors.New("example value cannot be nil") } -- 2.49.1 From 09749029b45fc86627e77cbaaa83eda9b1983883 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 26 Sep 2025 18:57:53 -0400 Subject: [PATCH 8/8] [new] types and logic - addressing array type - slice type now does random amount of entries - better comments/readme --- README.md | 103 ++++++++++++++++++++++++++++++++----------- lazy.go | 127 ++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 186 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 5ff9f23..13bc4ed 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,93 @@ # lazy -Lazy is a helper tool when working with SQLx and sqlmock that generates mock data based on the result struct. +Lazy is a helper tool when working with [sqlx](https://github.com/jmoiron/sqlx) and [sqlmock](https://github.com/DATA-DOG/go-sqlmock) that generates mock data for unit tests based on the example struct. -## GenerateRandomResults -This function expects the SQL query, the example response object, and an optional primary key to set. +
-Query The SQL query argument is taken and rebound using sqlx.AT bindvar type, this allows the returned query to be used directly in a sqlmock [ExpectQuery](https://pkg.go.dev/github.com/data-dog/go-sqlmock#Sqlmock.ExpectQuery) check. +- [RandomGenerate](#randomgenerate) + * [Config](#config) + * [Mocks](#mocks) + * [Tags](#tags) + * [Example](#example) -Example Object The example object argument must be a struct and requires "db" tags. The db tags are then parsed and used to calculate the psuedo-random rows and values +
-Primary Key The optional primary key argument is used to hardcode a primary key field in the returned mocks, the primary key field in the example struct must have a lazy tag with the value "key" - -Row Count Row count sets the amount of mock rows to generate, if set to 0 then one row will be generated. If primary keys are provided you must provide one per row. +## RandomGenerate +Generates random data for basic types and slices to be used with [sqlmock](https://github.com/DATA-DOG/go-sqlmock). Slices are given an arbitrary amount entries See [Mocks](#mocks) for more information. ```go -type Mock struct { - Field1 string `db:"field1" lazy:"key"` -} +func RandomGenerate(m Config) (*Mock, error) ``` -## Example -```go -func Test() { - testArg := 123 - query := `SELECT ...` - type results struct { - Field1 string `db:"field1" lazy:"key"` - Field2 int `db:"field2"` - } +
- rando, err := GenerateRandomResults(query, results{}, []any{testArg}, 1) +### Config +```go +type Config struct { + Query string + Example any + Keys []any + RowCount int +} +``` +The Config struct is used to generate the data: +- Query the sql query that is being run in the function being tested +- Example represents the struct that will be used in the sql scan, this struct is used to detect which fields are expected in the query. Fields must have "db" tag to be parsed +- Keys primary keys to be hardcoded in mock rows, if needed. Primary keys are detected by tags in the example struct, see [Tags](#tags) for more information +- RowCount the amount of mock rows to be generated, if you are setting primary keys this number must be equal to the amount of keys supplied + +
+ +### Mocks +```go +type Mock struct { + Query string + Columns []string + Rows [][]driver.Value +} +``` +The Mock struct is returned by RandomGenerate: +- Query represents the query string expected by [ExpectQuery](https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#SqlmockCommon.ExpectQuery). The original query is rebound using the bindvar type [sqlx.AT](https://pkg.go.dev/github.com/jmoiron/sqlx#AT) +- Columns slice of expected column names for [sqlmock.NewRows](https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#NewRows) +- Rows represents the row data for [AddRows](https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#Rows.AddRows) + +
+ +### Tags +Struct tags are required on the example struct to generate the data +- lazy optional, set this tag to key to mark the primary key field on the example struct, if no Keys are provided in the Config struct this tag will be ignored +- db required for mock columns to be generated, columns will be based on tag value + +
+ +### Example +This program is a psuedo-code example of the mock generation. It takes the SqlResultsExample and uses it to generate two rows of mock data using the Keys (1, 6) as the Key, signified by the lazy tag. See [Example output](#example-output-rows) for the generated sql rows. +```go +func ExampleTest() { + ... + + type SqlResultsExample struct { + Key string `db:"key" lazy:"key"` + Count int `db:"count"` + } + cfg := lazy.Config{ + Query: "SELECT key, count FROM table WHERE key IN (?, ?)", + Example: SqlResultsExample{}, + Keys: []any{ 1, 6 }, + RowCount: 2, + } + mock, err := lazy.GenerateRandom(cfg) if err != nil { panic(err) } - - //the rando object will now have psuedo random values in all fields except for the field containing the test tag set to "key" that field will be hardcoded with the testArg to allow for unit tests to ensure the requested ID flows through - rows := sqlmock.NewRows(rando.Columns).AddRows(rando.Rows...) - mock.ExpectQuery(rando.Query).WithArgs(testArg).WillReturnRows(rows) + rows := sqlmock.NewRows(mock.Columns).AddRows(mock.Rows...) + sqlmock.ExpectQuery(mock.Query).WithArgs(1, 6).WillReturnRows(rows) ... + } -``` \ No newline at end of file +``` +#### Example of mocked rows from ExampleTest +|key|count| +|---|-----| +|1|649018089772805963| +|6|1066940846275557682| diff --git a/lazy.go b/lazy.go index 51fa3f9..4a7bcc4 100644 --- a/lazy.go +++ b/lazy.go @@ -12,35 +12,45 @@ import ( ) const ( - DB_TAG = "db" - LAZY_TAG = "lazy" - KEY_VALUE = "key" + DB_TAG = "db" //tag used to parse sql fields in example struct + LAZY_TAG = "lazy" //tag label for KEY_VALUE + KEY_VALUE = "key" //tag value for LAZY_TAG ) -type MockResults struct { +// mock data generated based on config +type Mock struct { Query string Columns []string Rows [][]driver.Value } +// configuration for RandomGenerate type Config struct { Query string Example any - Keys []any + Keys []any //length of keys must = RowCount, if set RowCount int } -func GenerateRandomResults(m Config) (*MockResults, error) { +// generates mock data based on configuration to be used for sqlmock +func RandomGenerate(m Config) (*Mock, error) { + //example struct cannot be nil and must be a struct if m.Example == nil { return nil, errors.New("example value cannot be nil") } if reflect.ValueOf(m.Example).Kind() != reflect.Struct { return nil, errors.New("example value must be a struct") } - if m.RowCount == 0 { + + //any weirdness, just pull one row + if m.RowCount <= 0 { m.RowCount = 1 } - if len(m.Keys) != 0 { + + //if keys are set, ensure there are enough to populate requested rows + primaryKey := false + if len(m.Keys) > 0 { + primaryKey = true if len(m.Keys) != m.RowCount { return nil, errors.New("you must provide a key for each row") } @@ -60,15 +70,18 @@ func GenerateRandomResults(m Config) (*MockResults, error) { continue } + //track columns only once if y == 0 { columns = append(columns, dbTag) } - if field.Tag.Get(LAZY_TAG) == KEY_VALUE { + //if field has lazy:"key" tag and tags are present inject primary key + if field.Tag.Get(LAZY_TAG) == KEY_VALUE && primaryKey { rows[y] = append(rows[y], m.Keys[y]) continue } + //generate random values nv := kindToRandom(field) if nv == nil { return nil, fmt.Errorf("could not match type: %s", retType.Name()) @@ -78,13 +91,15 @@ func GenerateRandomResults(m Config) (*MockResults, error) { } } - return &MockResults{ + return &Mock{ + //sql is rebound and escaped for sqlmock.ExpectQuery Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(m.Query)), Columns: columns, Rows: rows, }, nil } +// converts basic reflect Kind's to psuedo-random data, slices are given a random amount of entries func kindToRandom(field reflect.StructField) any { kind := field.Type.Kind() switch kind { @@ -102,23 +117,99 @@ func kindToRandom(field reflect.StructField) any { return fmt.Sprintf("%d", rand.Int()) case reflect.Bool: return rand.Int()%2 == 0 - case reflect.Slice: + case reflect.Array: underlying := field.Type.Elem().Kind() + count := reflect.ValueOf(field).Len() //fill entire length switch underlying { case reflect.Int: - return []int{rand.Int()} + var intslice []int + for x := 0; x < count; x++ { + intslice = append(intslice, rand.Int()) + } + return intslice case reflect.Int32: - return []int32{rand.Int32()} + var int32slice []int32 + for x := 0; x < count; x++ { + int32slice = append(int32slice, rand.Int32()) + } + return int32slice case reflect.Int64: - return []int64{rand.Int64()} + var int64slice []int64 + for x := 0; x < count; x++ { + int64slice = append(int64slice, rand.Int64()) + } + return int64slice case reflect.Float32: - return []float32{rand.Float32()} + var float32slice []float32 + for x := 0; x < count; x++ { + float32slice = append(float32slice, rand.Float32()) + } + return float32slice case reflect.Float64: - return []float64{rand.Float64()} + var float64slice []float64 + for x := 0; x < count; x++ { + float64slice = append(float64slice, rand.Float64()) + } + return float64slice case reflect.String: - return []string{fmt.Sprintf("%d", rand.Int())} + var strslice []string + for x := 0; x < count; x++ { + strslice = append(strslice, fmt.Sprintf("%d", rand.Int())) + } + return strslice case reflect.Bool: - return []bool{rand.Int()%2 == 0} + var boolslice []bool + for x := 0; x < count; x++ { + boolslice = append(boolslice, (rand.Int()%2 == 0)) + } + return boolslice + } + case reflect.Slice: + underlying := field.Type.Elem().Kind() + count := (rand.Int() % 10) + 1 //amount of entries to append to slice + switch underlying { + case reflect.Int: + var intslice []int + for x := 0; x < count; x++ { + intslice = append(intslice, rand.Int()) + } + return intslice + case reflect.Int32: + var int32slice []int32 + for x := 0; x < count; x++ { + int32slice = append(int32slice, rand.Int32()) + } + return int32slice + case reflect.Int64: + var int64slice []int64 + for x := 0; x < count; x++ { + int64slice = append(int64slice, rand.Int64()) + } + return int64slice + case reflect.Float32: + var float32slice []float32 + for x := 0; x < count; x++ { + float32slice = append(float32slice, rand.Float32()) + } + return float32slice + case reflect.Float64: + var float64slice []float64 + for x := 0; x < count; x++ { + float64slice = append(float64slice, rand.Float64()) + } + return float64slice + case reflect.String: + var strslice []string + for x := 0; x < count; x++ { + strslice = append(strslice, fmt.Sprintf("%d", rand.Int())) + } + return strslice + case reflect.Bool: + var boolslice []bool + for x := 0; x < count; x++ { + boolslice = append(boolslice, (rand.Int()%2 == 0)) + } + return boolslice } } -- 2.49.1