27 Commits

Author SHA1 Message Date
77b8bdf218 removing driver, just generating mock struct args would need an order 2025-09-30 17:48:22 -04:00
2161c201bc reimplementing db tag 2025-09-30 17:39:40 -04:00
935285a9f3 testing without db tags 2025-09-30 17:39:15 -04:00
5f24529730 proper slice allocation 2025-09-30 17:32:27 -04:00
5563e93efd working locally 2025-09-30 17:23:31 -04:00
d0ec9465e2 testing new length 2025-09-30 17:17:45 -04:00
0c4a1abb9a testing any typecast 2025-09-30 17:15:45 -04:00
791ffad0e3 testing 2025-09-30 17:13:52 -04:00
2415af38b2 Fixing struct generation
- ready to test
- slice size bugfixes
- appending empty struct before filling it out
2025-09-30 17:09:19 -04:00
053f5b5fd8 no more index check 2025-09-30 16:25:54 -04:00
6326711424 adding ok check to prevent panics 2025-09-30 11:47:38 -04:00
6a9cdafcf1 testing better types 2025-09-30 11:43:40 -04:00
5e2ddecdcc dont elem slice 2025-09-30 11:41:28 -04:00
9033530a55 testing any type for resturned structs 2025-09-30 11:33:59 -04:00
6bbb452b03 argument type fix 2025-09-30 11:26:54 -04:00
21b1291dbc struct name revert 2025-09-30 11:20:30 -04:00
c62da47a31 ready for tests 2025-09-30 11:12:47 -04:00
ff62b371c9 adding new functionality for mocks
- working through insert logic
- ideally return struct with 'good' fields
- also return list of args to prevent relooping
2025-09-29 15:47:51 -04:00
3b40f8061f Merge pull request 'fix/allow-many-rows' (#1) from fix/allow-many-rows into main
Reviewed-on: #1
2025-09-26 22:58:50 +00:00
09749029b4 [new] types and logic
- addressing array type
- slice type now does random amount of entries
- better comments/readme
2025-09-26 18:57:53 -04:00
4febf3d877 struct rename 2025-09-26 16:48:53 -04:00
4573594c85 moving to struct as param, readme updates 2025-09-26 16:46:40 -04:00
b9a8c5cd47 slice indexing 2025-09-26 16:33:24 -04:00
92b673cb46 persisting primary keys through rows 2025-09-26 16:29:36 -04:00
75f6ef8b5b moving to append 2025-09-26 16:25:46 -04:00
428381545d adding proper allocations 2025-09-26 16:18:27 -04:00
891cd03b5a adding support for returning more than one mock row 2025-09-26 16:16:53 -04:00
3 changed files with 309 additions and 60 deletions

2
.gitignore vendored
View File

@@ -25,3 +25,5 @@ go.work.sum
# env file
.env
#ignore test files
cmd/

View File

@@ -1,40 +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.
<hr />
<b>Query</b> 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)
<b>Example Object</b> The example object argument <u>must</u> be a struct and requires "db" tags. The db tags are then parsed and used to calculate the psuedo-random rows and values
<hr />
<b>Primary Key</b> 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 <u>must</u> have a test tag with the value "key"
## 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" test:"key"`
}
func RandomGenerate(m Config) (*Mock, error)
```
## Example
```go
func Test() {
testArg := 123
query := `SELECT ...`
type results struct {
Field1 string `db:"field1" test:"key"`
Field2 int `db:"field2"`
}
<hr />
rando, err := GenerateRandomResults(query, results{}, testArg)
### Config
```go
type Config struct {
Query string
Example any
Keys []any
RowCount int
}
```
The Config struct is used to generate the data:
- <b>Query</b> the sql query that is being run in the function being tested
- <b>Example</b> 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
- <b>Keys</b> 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
- <b>RowCount</b> 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
<hr />
### Mocks
```go
type Mock struct {
Query string
Columns []string
Rows [][]driver.Value
}
```
The Mock struct is returned by RandomGenerate:
- <b>Query</b> 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)
- <b>Columns</b> slice of expected column names for [sqlmock.NewRows](https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#NewRows)
- <b>Rows</b> represents the row data for [AddRows](https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#Rows.AddRows)
<hr />
### Tags
Struct tags are required on the example struct to generate the data
- <b>lazy</b> optional, set this tag to <b>key</b> to mark the primary key field on the example struct, if no Keys are provided in the Config struct this tag will be ignored
- <b>db</b> required for mock columns to be generated, columns will be based on tag value
<hr />
### 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).AddRow(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)
...
}
```
#### Example of mocked rows from ExampleTest
|key|count|
|---|-----|
|1|649018089772805963|
|6|1066940846275557682|

238
lazy.go
View File

@@ -11,51 +11,169 @@ import (
"github.com/jmoiron/sqlx"
)
type MockResults struct {
const (
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
)
var (
ErrBadExample = errors.New("example field must be a non-nil struct")
ErrMissingKeys = errors.New("there must be a key for each row requested with RowCount")
//contains placeholder to inject type on runtime
ErrUnsupportedType = errors.New("type: %s is not yet supported")
)
// mock data generated based on config
type RowMock struct {
Query string
Columns []string
Rows []driver.Value
Rows [][]driver.Value
}
func GenerateRandomResults(query string, exampleObj any, keyVal any) (*MockResults, error) {
if exampleObj == nil {
return nil, errors.New("exampleObj cannot be nil")
// configuration for RandomGenerate
type RowConfig struct {
Query string
Example any
Keys []any
RowCount int
}
retType := reflect.TypeOf(exampleObj)
type StructConfig struct {
Query string
Example any
RowCount int
}
type StructMock struct {
Query string
MockStructs []any
}
// generates mock data based on configuration to be used for sqlmock
func RandomRows(r RowConfig) (*RowMock, error) {
//example struct cannot be nil and must be a struct
if r.Example == nil {
return nil, ErrBadExample
}
if reflect.ValueOf(r.Example).Kind() != reflect.Struct {
return nil, ErrBadExample
}
//any weirdness, just pull one row
if r.RowCount <= 0 {
r.RowCount = 1
}
//if keys are set, ensure there are enough to populate requested rows
primaryKey := false
if len(r.Keys) > 0 {
primaryKey = true
if len(r.Keys) != r.RowCount {
return nil, ErrMissingKeys
}
}
retType := reflect.TypeOf(r.Example)
maxFieldCount := retType.NumField()
columns := make([]string, 0, maxFieldCount)
rows := make([]driver.Value, 0, maxFieldCount)
rows := make([][]driver.Value, 0)
for y := 0; y < r.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
}
//track columns only once
if y == 0 {
columns = append(columns, dbTag)
}
if field.Tag.Get("test") == "key" {
rows = append(rows, keyVal)
//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], r.Keys[y])
continue
}
//generate random values
nv := kindToRandom(field)
if nv == nil {
return nil, fmt.Errorf("could not match type: %s", retType.Name())
return nil, fmt.Errorf(ErrUnsupportedType.Error(), field.Type.Name())
}
rows = append(rows, nv)
rows[y] = append(rows[y], nv)
}
}
return &MockResults{
Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(query)),
return &RowMock{
//sql is rebound and escaped for sqlmock.ExpectQuery
Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(r.Query)),
Columns: columns,
Rows: rows,
}, nil
}
// generates mock data for insert statements to be used for sqlmock, it also returns a slice of
// example structs with mock data
func RandomStruct(c StructConfig) (*StructMock, error) {
//make sure we have an example struct
if c.Example == nil {
return nil, ErrBadExample
}
if reflect.ValueOf(c.Example).Kind() != reflect.Struct {
return nil, ErrBadExample
}
//we need at least one row
if c.RowCount <= 0 {
c.RowCount = 1
}
//get example type and number of fields
retType := reflect.TypeOf(c.Example)
maxFieldCount := retType.NumField()
//create slice of example structs
ft := reflect.SliceOf(retType)
filled := reflect.MakeSlice(ft, 0, c.RowCount)
for x := 0; x < c.RowCount; x++ {
//add empty example struct
filled = reflect.Append(filled, reflect.ValueOf(c.Example))
for y := 0; y < maxFieldCount; y++ {
field := retType.Field(y)
//get random value
nv := kindToRandom(field)
if nv == nil {
return nil, fmt.Errorf(ErrUnsupportedType.Error(), field.Type.Name())
}
if filled.Index(x).Field(y).CanSet() {
filled.Index(x).Field(y).Set(reflect.ValueOf(nv))
}
}
}
//convert reflect.Value to []any
fl := filled.Len()
retStructs := make([]any, fl)
for x := 0; x < fl; x++ {
retStructs[x] = filled.Index(x).Interface()
}
return &StructMock{
Query: sqlx.Rebind(sqlx.AT, regexp.QuoteMeta(c.Query)),
MockStructs: retStructs,
}, nil
}
// converts basic reflect Kind's to psuedo-random data, it is super basic and supports very basic types. Slices and arrays
// are supported, slices will get 1-10 entries and arrays will fill length
func kindToRandom(field reflect.StructField) any {
kind := field.Type.Kind()
switch kind {
@@ -73,23 +191,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
}
}