Skip to content

Commit 60864dc

Browse files
authored
Merge pull request #1 from Selvatico/master
Update locally
2 parents 7fe2a85 + b7a2ceb commit 60864dc

File tree

2 files changed

+276
-2
lines changed

2 files changed

+276
-2
lines changed

DOCUMENTATION.md

+274
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
# go-mocket Documentation
2+
3+
## Setting Up Tests
4+
5+
***
6+
7+
To set up tests, you need to register the driver and override the DB instance used across the code base.
8+
```go
9+
import (
10+
"database/sql"
11+
mocket "github.com/selvatico/go-mocket"
12+
"github.com/jinzhu/gorm"
13+
)
14+
15+
func SetupTests() *sql.DB { // or *gorm.DB
16+
mocket.Catcher.Register() // Safe register. Allowed multiple calls to save
17+
// GORM
18+
db, err := gorm.Open(mocket.DRIVER_NAME, "connection_string") // Can be any connection string
19+
DB = db
20+
21+
// OR
22+
// Regular sql package usage
23+
db, err := sql.Open(mocket.DRIVER_NAME, "connection_string")
24+
25+
return db
26+
}
27+
```
28+
29+
In the snippet above, we intentionally skipped assigning to proper variable DB instance. One of the assumptions is that the project has one DB instance at the time, overriding it with FakeDriver will do the job.
30+
31+
## Simple Chain Usage
32+
33+
***
34+
35+
```go
36+
// Function to tests
37+
func GetUsers(db *sql.DB) []map[string]string {
38+
var res []map[string]string
39+
age := 27
40+
rows, err := db.Query("SELECT name FROM users WHERE age=?", age)
41+
if err != nil {
42+
log.Fatal(err)
43+
}
44+
defer rows.Close()
45+
for rows.Next() {
46+
var name string
47+
var age string
48+
if err := rows.Scan(&name, &age); err != nil {
49+
log.Fatal(err)
50+
}
51+
row := map[string]string{"name": name, "age": age}
52+
res = append(res, row)
53+
}
54+
if err := rows.Err(); err != nil {
55+
log.Fatal(err)
56+
}
57+
return res
58+
}
59+
60+
// Test function
61+
func TestResponses(t *testing.T) {
62+
SetupTests()
63+
64+
t.Run("Simple SELECT caught by query", func(t *testing.T) {
65+
Catcher.Logging = false
66+
commonReply := []map[string]interface{}{{"user_id": 27, "name": "FirstLast", "age": "30"}}
67+
Catcher.Reset().NewMock().WithQuery(`SELECT name FROM users WHERE`).WithReply(commonReply)
68+
result := GetUsers(DB) // Global or local variable
69+
if len(result) != 1 {
70+
t.Errorf("Returned sets is not equal to 1. Received %d", len(result))
71+
}
72+
if result[0]["age"] != "30" {
73+
t.Errorf("Age is not equal. Got %v", result[0]["age"])
74+
}
75+
})
76+
}
77+
```
78+
In the example above, we create a new mock via `.NewMock()` and attach a query pattern which will be used to catch a matched query. `.WithReply()` specifies which response will be provided during the mock of this request.
79+
As `Catcher` is global variable without calling `.Reset()` this mock will be applied to all subsequent tests and queries if the pattern matches.
80+
81+
## Usage via `FakeResponse` Object
82+
83+
We are taking `GetUsers` from the previous example and an example on how it can be using a FakeResponse directly attached to the Catcher object
84+
85+
```go
86+
t.Run("Simple select with direct object", func(t *testing.T) {
87+
Catcher.Reset()
88+
commonReply := []map[string]interface{}{{"user_id": 27, "name": "FirstLast", "age": "30"}}
89+
Catcher.Attach([]*FakeResponse{
90+
{
91+
Pattern:"SELECT name FROM users WHERE", // the same as .WithQuery()
92+
Response: commonReply, // the same as .WithReply
93+
Once: false, // To not use it twice if true
94+
},
95+
})
96+
result := GetUsers(DB)
97+
if len(result) != 1 {
98+
t.Errorf("Returned sets is not equal to 1. Received %d", len(result))
99+
}
100+
if result[0]["age"] != "30" {
101+
t.Errorf("Age is not equal. Got %v", result[0]["age"])
102+
}
103+
})
104+
```
105+
106+
## GORM Example
107+
108+
Usage of a mocked GORM is completely transparent. You need to know which query will be generated by GORM and mock them or mock just by using arguments. In this case, you need to pay attention to order of arguments as GORM will not necessarily arrange them in order you provided them.
109+
110+
*Tip:* To identify the exact query generated by GORM you can look at the console output when running your mocked DB connection. They show up like this:
111+
```
112+
2018/01/01 12:00:01 mock_catcher: check query: INSERT INTO "users" ("name") VALUES (?)
113+
```
114+
115+
116+
## More Examples
117+
118+
***
119+
120+
### Catch by Arguments
121+
122+
The query can be caught by provided arguments even you are not specifying a query pattern to match.
123+
Please note these two important facts:
124+
125+
* Order is very important
126+
* GORM will re-order arguments according to fields in the struct defined to describe your model.
127+
128+
```go
129+
t.Run("Catch by arguments", func(t *testing.T) {
130+
commonReply := []map[string]interface{}{{"name": "FirstLast", "age": "30"}}
131+
Catcher.Reset().NewMock().WithArgs(int64(27)).WithReply(commonReply)
132+
result := GetUsers(DB)
133+
if len(result) != 1 {
134+
t.Fatalf("Returned sets is not equal to 1. Received %d", len(result))
135+
}
136+
// all other checks from reply
137+
})
138+
```
139+
140+
### Match Only Once
141+
142+
Mocks marked as Once, will not be match on subsequent queries.
143+
```go
144+
t.Run("Once", func(t *testing.T) {
145+
Catcher.Reset()
146+
Catcher.Attach([]*FakeResponse{
147+
{
148+
Pattern:"SELECT name FROM users WHERE",
149+
Response: commonReply,
150+
Once: true, // could be done via chaining .OneTime()
151+
},
152+
})
153+
GetUsers(DB) // Trigger once to use this mock
154+
result := GetUsers(DB) // trigger second time to receive empty results
155+
if len(result) != 0 {
156+
t.Errorf("Returned sets is not equal to 0. Received %d", len(result))
157+
}
158+
})
159+
```
160+
161+
### Insert ID with `.WithId(int64)`
162+
163+
In order to emulate `INSERT` requests, we can mock the ID returned from the query with the `.WithId(int64)` method.
164+
165+
```go
166+
// Somewhere in the code
167+
func InsertRecord(db *sql.DB) int64 {
168+
res, err := db.Exec(`INSERT INTO foo VALUES("bar", ?))`, "value")
169+
if err != nil {
170+
return 0
171+
}
172+
id, _ := res.LastInsertId()
173+
return id
174+
}
175+
176+
// Test code
177+
t.Run("Last insert id", func(t *testing.T) {
178+
var mockedId int64
179+
mockedId = 64
180+
Catcher.Reset().NewMock().WithQuery("INSERT INTO foo").WithId(mockedId)
181+
returnedId := InsertRecord(DB)
182+
if returnedId != mockedId {
183+
t.Fatalf("Last insert id not returned. Expected: [%v] , Got: [%v]", mockedId, returnedId)
184+
}
185+
})
186+
```
187+
188+
### Emulate Exceptions
189+
190+
You can emulate exceptions or errors during the request by setting it with a fake `FakeResponse` object.
191+
Please note that to fire an error on `SELECT` you need to use `WithQueryException()`, for other queries (UPDATE, DELETE, etc) which do not return results, you need to use `.WithExecException()`.
192+
193+
Example:
194+
```go
195+
// Somewhere in the code
196+
func GetUsersWithError(db *sql.DB) error {
197+
age := 27
198+
_, err := db.Query("SELECT name FROM users WHERE age=?", age)
199+
return err
200+
}
201+
202+
func CreateUsersWithError(db *sql.DB) error {
203+
age := 27
204+
_, err := db.Query("INSERT INTO users (age) VALUES (?) ", age)
205+
return err
206+
}
207+
208+
// Test
209+
t.Run("Fire Query error", func(t *testing.T) {
210+
Catcher.Reset().NewMock().WithArgs(int64(27)).WithQueryException()
211+
err := GetUsersWithError(DB)
212+
if err == nil {
213+
t.Fatal("Error not triggered")
214+
}
215+
})
216+
217+
t.Run("Fire Execute error", func(t *testing.T) {
218+
Catcher.Reset().NewMock().WithQuery("INSERT INTO users (age)").WithQueryException()
219+
err := CreateUsersWithError(DB)
220+
if err == nil {
221+
t.Fatal("Error not triggered")
222+
}
223+
})
224+
```
225+
226+
### Callbacks
227+
228+
Besides that, you can catch and attach callbacks when the mock is used.
229+
230+
## Code Gotchas
231+
232+
### Query Matching
233+
When you try to match against a query, you have to make sure that you do so with precision.
234+
235+
For example, this query :
236+
```sql
237+
SELECT * FROM "users" WHERE ("users"."user_id" = 3) ORDER BY "users"."user_id" ASC LIMIT 1
238+
```
239+
240+
If you try to match it with this:
241+
```go
242+
Catcher.Reset().NewMock().WithQuery(`SELECT * FROM users WHERE`).WithReply(commonReply)
243+
```
244+
It will not work for two reasons. `users` is missing double-quotes and there are two spaces before `WHERE`. One trick is to actually run the test and look at the mocked DB output to find the exact query being executed.
245+
Here is the right way to match this query:
246+
247+
```go
248+
Catcher.Reset().NewMock().WithQuery(`SELECT * FROM "users" WHERE`).WithReply(commonReply)
249+
```
250+
251+
### Reply Matching
252+
When you provide a Reply to Catcher, your field names must match your database model or else, they will not be updated with the right value.
253+
254+
Given you have this test code:
255+
```go
256+
// DO NOT USE, CODE NOT WORKING
257+
commonReply := []map[string]interface{}{{"userID": 7, "name": "FirstLast", "age": "30"}}
258+
mocket.Catcher.NewMock().OneTime().WithQuery(`SELECT * FROM "dummies"`).WithReply(commonReply)
259+
260+
result := GetUsers(DB)
261+
```
262+
263+
This will work and not error out, but `result` will have a 0 value in the field `userID`. You must make sure to match the Reply fields with the database fields and not the struct fields or else you might bang your head on your keyboard.
264+
265+
The following code works:
266+
```go
267+
commonReply := []map[string]interface{}{{"user_id": 7, "name": "FirstLast", "age": "30"}}
268+
mocket.Catcher.NewMock().OneTime().WithQuery(`SELECT * FROM "dummies"`).WithReply(commonReply)
269+
270+
result := GetUsers(DB)
271+
```
272+
273+
__More examples coming....__
274+

response_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ func TestResponses(t *testing.T) {
6565
if len(result) != 1 {
6666
t.Errorf("Returned sets is not equal to 1. Received %d", len(result))
6767
}
68-
if result[0]["age"] != "30" {
69-
t.Errorf("Age is not equal. Got %v", result[0]["age"])
68+
if result[0]["name"] != "FirstLast" {
69+
t.Errorf("Name is not equal. Got %v", result[0]["name"])
7070
}
7171
})
7272

0 commit comments

Comments
 (0)