Skip to content

Latest commit

 

History

History
296 lines (241 loc) · 9.44 KB

DOCUMENTATION.md

File metadata and controls

296 lines (241 loc) · 9.44 KB

Documentation

Setting Up Tests


To set up tests, you need to register the driver and override the DB instance used across the code base.

import (
  "database/sql"
  mocket "github.com/selvatico/go-mocket"
  "github.com/jinzhu/gorm"
)

func SetupTests() *sql.DB { // or *gorm.DB
  mocket.Catcher.Register() // Safe register. Allowed multiple calls to save
  mocket.Catcher.Logging = true
  // GORM
  db, err := gorm.Open(mocket.DriverName, "connection_string") // Can be any connection string
  DB = db

   // OR
   // Regular sql package usage
   db, err := sql.Open(mocket.DriverName, "connection_string")

   return db
}

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.

Usage


There are two possible ways to use mocket:

  • Chaining API
  • Specifying FakeResponse object with all fields manually. Could be useful for cases when mocks stored separately as the list of FakeResponses.

Simple Chain Usage

// Function to tests
func GetUsers(db *sql.DB) []map[string]string {
	var res []map[string]string
	age := 27
	rows, err := db.Query("SELECT name FROM users WHERE age=?", age)
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()
	for rows.Next() {
		var name string
		var age string
		if err := rows.Scan(&name, &age); err != nil {
			log.Fatal(err)
		}
		row := map[string]string{"name": name, "age": age}
		res = append(res, row)
	}
	if err := rows.Err(); err != nil {
		log.Fatal(err)
	}
	return res
}

// Test function
func TestResponses(t *testing.T) {
	SetupTests()

	t.Run("Simple SELECT caught by query", func(t *testing.T) {
		Catcher.Logging = true
		// Important: Use database files here (snake_case) and not struct variables (CamelCase)
		// eg: first_name, last_name, date_of_birth NOT FirstName, LastName or DateOfBirth
		commonReply := []map[string]interface{}{{"user_id": 27, "name": "FirstLast", "age": "30"}}
		Catcher.Reset().NewMock().WithQuery(`SELECT name FROM users WHERE`).WithReply(commonReply)
		result := GetUsers(DB) // Global or local variable
		if len(result) != 1 {
			t.Errorf("Returned sets is not equal to 1. Received %d", len(result))
		}
		if result[0]["age"] != "30" {
			t.Errorf("Age is not equal. Got %v", result[0]["age"])
		}
	})
}

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. As Catcher is global variable without calling .Reset() this mock will be applied to all subsequent tests and queries if the pattern matches.

Usage via FakeResponse Object

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

t.Run("Simple select with direct object", func(t *testing.T) {
	Catcher.Reset()
	Catcher.Logging = true
	// Important: Use database files here (snake_case) and not struct variables (CamelCase)
	// eg: first_name, last_name, date_of_birth NOT FirstName, LastName or DateOfBirth
	commonReply := []map[string]interface{}{{"user_id": 27, "name": "FirstLast", "age": "30"}}
	Catcher.Attach([]*FakeResponse{
		{
			Pattern:"SELECT name FROM users WHERE", // the same as .WithQuery()
			Response: commonReply, // the same as .WithReply
			Once: false, // To not use it twice if true
		},
	})
	result := GetUsers(DB)
	if len(result) != 1 {
		t.Errorf("Returned sets is not equal to 1. Received %d", len(result))
	}
	if result[0]["age"] != "30" {
		t.Errorf("Age is not equal. Got %v", result[0]["age"])
	}
})

GORM Example


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.

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:

2018/01/01 12:00:01 mock_catcher: check query: INSERT INTO "users" ("name") VALUES (?)

Just make sure you enable logging like so:

Catcher.Logging = true

More Examples


Catch by Arguments

The query can be caught by provided arguments even you are not specifying a query pattern to match. Please note these two important facts:

  • Order is very important
  • GORM will re-order arguments according to fields in the struct defined to describe your model.
t.Run("Catch by arguments", func(t *testing.T) {
	// Important: Use database files here (snake_case) and not struct variables (CamelCase)
	// eg: first_name, last_name, date_of_birth NOT FirstName, LastName or DateOfBirth
    commonReply := []map[string]interface{}{{"name": "FirstLast", "age": "30"}}
    Catcher.Reset().NewMock().WithArgs(int64(27)).WithReply(commonReply)
    result := GetUsers(DB)
    if len(result) != 1 {
	    t.Fatalf("Returned sets is not equal to 1. Received %d", len(result))
    }
    // all other checks from reply
})

Match Only Once

Mocks marked as Once, will not be match on subsequent queries.

t.Run("Once", func(t *testing.T) {
	Catcher.Reset()
	// Important: Use database files here (snake_case) and not struct variables (CamelCase)
	// eg: first_name, last_name, date_of_birth NOT FirstName, LastName or DateOfBirth
	commonReply := []map[string]interface{}{{"name": "FirstLast"}}
	
	Catcher.Attach([]*FakeResponse{
		{
			Pattern:"SELECT name FROM users WHERE",
			Response: commonReply,
			Once: true, // could be done via chaining .OneTime()
		},
	})
	GetUsers(DB) // Trigger once to use this mock
	result := GetUsers(DB) // trigger second time to receive empty results
	if len(result) != 0 {
		t.Errorf("Returned sets is not equal to 0. Received %d", len(result))
	}
})

Insert ID with .WithID(int64)

In order to emulate INSERT requests, we can mock the ID returned from the query with the .WithID(int64) method.

// Somewhere in the code
func InsertRecord(db *sql.DB) int64  {
   res, err := db.Exec(`INSERT INTO foo VALUES("bar", ?))`, "value")
   if err != nil {
     return 0
   }
   id, _ := res.LastInsertID()
   return id
}

// Test code
t.Run("Last insert id", func(t *testing.T) {
   var mockedId int64
   mockedId = 64
   Catcher.Reset().NewMock().WithQuery("INSERT INTO foo").WithID(mockedId)
   returnedId := InsertRecord(DB)
   if returnedId != mockedId {
     t.Fatalf("Last insert id not returned. Expected: [%v] , Got: [%v]", mockedId, returnedId)
   }
})

Emulate Exceptions

You can emulate exceptions or errors during the request by setting it with a fake FakeResponse object. 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().

Example:

// Somewhere in the code
func GetUsersWithError(db *sql.DB) error {
  age := 27
  _, err := db.Query("SELECT name FROM users WHERE age=?", age)
  return err
}

func CreateUsersWithError(db *sql.DB) error {
	age := 27
	_, err := db.Query("INSERT INTO users (age) VALUES (?) ", age)
	return err
}

// Test
t.Run("Fire Query error", func(t *testing.T) {
	Catcher.Reset().NewMock().WithArgs(int64(27)).WithQueryException()
	err := GetUsersWithError(DB)
	if err == nil {
	   t.Fatal("Error not triggered")
	}
})

t.Run("Fire Execute error", func(t *testing.T) {
   Catcher.Reset().NewMock().WithQuery("INSERT INTO users (age)").WithQueryException()
   err := CreateUsersWithError(DB)
   if err == nil {
	 t.Fatal("Error not triggered")
   }
})

Callbacks

Besides that, you can catch and attach callbacks when the mock is used.

Code Gotchas

Query Matching

When you try to match against a query, you have to make sure that you do so with precision.

For example, this query :

SELECT * FROM "users"  WHERE ("users"."user_id" = 3) ORDER BY "users"."user_id" ASC LIMIT 1

If you try to match it with this:

Catcher.Reset().NewMock().WithQuery(`SELECT * FROM users WHERE`).WithReply(commonReply)

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. Here is the right way to match this query:

Catcher.Reset().NewMock().WithQuery(`SELECT * FROM "users"  WHERE`).WithReply(commonReply)

Reply Matching

When you provide a Reply to Catcher, your field names must match your database model and NOT the struct object or else, they will not be updated with the right value.

Given you have this test code:

    // *** DO NOT USE, CODE NOT WORKING ***
    commonReply := []map[string]interface{}{{"userID": 7, "name": "FirstLast", "age": "30"}}
	mocket.Catcher.NewMock().OneTime().WithQuery(`SELECT * FROM "dummies"`).WithReply(commonReply)

	result := GetUsers(DB)

This will seem to 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.

The following code works:

    commonReply := []map[string]interface{}{{"user_id": 7, "name": "FirstLast", "age": "30"}}
	mocket.Catcher.NewMock().OneTime().WithQuery(`SELECT * FROM "dummies"`).WithReply(commonReply)

	result := GetUsers(DB)