Compare commits
27 Commits
v1.0.0
...
new/insert
Author | SHA1 | Date | |
---|---|---|---|
77b8bdf218 | |||
2161c201bc | |||
935285a9f3 | |||
5f24529730 | |||
5563e93efd | |||
d0ec9465e2 | |||
0c4a1abb9a | |||
791ffad0e3 | |||
2415af38b2 | |||
053f5b5fd8 | |||
6326711424 | |||
6a9cdafcf1 | |||
5e2ddecdcc | |||
9033530a55 | |||
6bbb452b03 | |||
21b1291dbc | |||
c62da47a31 | |||
ff62b371c9 | |||
3b40f8061f | |||
09749029b4 | |||
4febf3d877 | |||
4573594c85 | |||
b9a8c5cd47 | |||
92b673cb46 | |||
75f6ef8b5b | |||
428381545d | |||
891cd03b5a |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -25,3 +25,5 @@ go.work.sum
|
||||
# env file
|
||||
.env
|
||||
|
||||
#ignore test files
|
||||
cmd/
|
101
README.md
101
README.md
@@ -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|
|
||||
|
266
lazy.go
266
lazy.go
@@ -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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
retType := reflect.TypeOf(exampleObj)
|
||||
//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 x := 0; x < maxFieldCount; x++ {
|
||||
field := retType.Field(x)
|
||||
dbTag := field.Tag.Get("db")
|
||||
if dbTag == "" {
|
||||
continue
|
||||
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_TAG)
|
||||
if dbTag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
//track columns only once
|
||||
if y == 0 {
|
||||
columns = append(columns, dbTag)
|
||||
}
|
||||
|
||||
//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(ErrUnsupportedType.Error(), field.Type.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{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user