redigomock

package module
v0.0.0-...-3485e45 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 14, 2015 License: GPL-2.0 Imports: 5 Imported by: 0

README

redigomock

Build Status GoDoc

Easy way to unit test projects using redigo library (Redis client in go). You can find the latest release here.

install

go get -u github.com/rafaeljusto/redigomock

usage

Here is an example of using redigomock, for more information please check the API documentation.

package main

import (
	"fmt"
	"github.com/garyburd/redigo/redis"
	"github.com/rafaeljusto/redigomock"
)

type Person struct {
	Name string `redis:"name"`
	Age  int    `redis:"age"`
}

func RetrievePerson(conn redis.Conn, id string) (Person, error) {
	var person Person

	values, err := redis.Values(conn.Do("HGETALL", fmt.Sprintf("person:%s", id)))
	if err != nil {
		return person, err
	}

	err = redis.ScanStruct(values, &person)
	return person, err
}

func main() {
	// Simulate command result

	redigomock.Command("HGETALL", "person:1").ExpectMap(map[string]string{
		"name": "Mr. Johson",
		"age":  "42",
	})

	person, err := RetrievePerson(redigomock.NewConn(), "1")
	if err != nil {
		fmt.Println(err)
		return
	}

	if person.Name != "Mr. Johson" {
		fmt.Printf("Invalid name. Expected 'Mr. Johson' and got '%s'\n", person.Name)
		return
	}

	if person.Age != 42 {
		fmt.Printf("Invalid age. Expected '42' and got '%d'\n", person.Age)
		return
	}

	// Simulate command error

	redigomock.Command("HGETALL", "person:1").ExpectError(fmt.Errorf("Simulate error!"))

	person, err = RetrievePerson(redigomock.NewConn(), "1")
	if err == nil {
		fmt.Println("Should return an error!")
		return
	}

	fmt.Println("Success!")
}

Documentation

Overview

Package redigomock is a mock for redigo library (redis client)

Redigomock basically register the commands with the expected results in a internal global variable. When the command is executed via Conn interface, the mock will look to this global variable to retrieve the corresponding result.

To start a mocked connection just do the following:

c := redigomock.NewConn()

Now you can inject it whenever your system needs a redigo.Conn because it satisfies all interface requirements. Before running your tests you need beyond of mocking the connection, registering the expected results. For that you can generate commands with the expected results.

redigomock.Command("HGETALL", "person:1").Expect("Person!")
redigomock.Command(
  "HMSET", []string{"person:1", "name", "John"},
).Expect("ok")

As the Expect method from Command receives anything (interface{}), another method was created to easy map the result to your structure. For that use ExpectMap:

redigomock.Command("HGETALL", "person:1").ExpectMap(map[string]string{
  "name": "John",
  "age": 42,
})

You should also test the error cases, and you can do it in the same way of a normal result.

redigomock.Command("HGETALL", "person:1").ExpectError(fmt.Errorf("Low level error!"))

Sometimes you will want to register a command regardless the arguments, and you can do it with the method GenericCommand (mainly with the HMSET).

redigomock.GenericCommand("HMSET").Expect("ok")

All commands are registered in a global variable, so they will be there until all your test cases ends. So for good practice in test writing you should in the beginning of each test case clear the mock states.

redigomock.Clear()

Let's see a full test example. Imagine a Person structure and a function that pick up this person in Redis using redigo library (file person.go):

 package person

 import (
	 "fmt"
 	"github.com/garyburd/redigo/redis"
 )

 type Person struct {
 	Name string `redis:"name"`
 	Age  int    `redis:"age"`
 }

 func RetrievePerson(conn redis.Conn, id string) (Person, error) {
 	var person Person

 	values, err := redis.Values(conn.Do("HGETALL", fmt.Sprintf("person:%s", id)))
 	if err != nil {
 		return person, err
 	}

 	err = redis.ScanStruct(values, &person)
 	return person, err
 }

Now we need to test it, so let's create the corresponding test with redigomock (fileperson_test.go):

 package person

 import (
   "github.com/rafaeljusto/redigomock"
   "testing"
 )

 func TestRetrievePerson(t *testing.T) {
   redigomock.Clear()
	  redigomock.Command("HGETALL", "person:1").ExpectMap(map[string]string{
	    "name": "Mr. Johson",
     "age":  "42",
   })

   person, err := RetrievePerson(redigomock.NewConn(), "1")
   if err != nil {
	    t.Fatal(err)
   }

   if person.Name != "Mr. Johson" {
	    t.Errorf("Invalid name. Expected 'Mr. Johson' and got '%s'", person.Name)
   }

   if person.Age != 42 {
	    t.Errorf("Invalid age. Expected '42' and got '%d'", person.Age)
   }
 }

 func TestRetrievePersonError(t *testing.T) {
   redigomock.Clear()
   redigomock.Command("HGETALL", "person:1").ExpectError(fmt.Errorf("Simulate error!"))

   person, err = RetrievePerson(redigomock.NewConn(), "1")
   if err == nil {
	    t.Error("Should return an error!")
   }
 }

When you use redis as a persistent list, then you might want to call the same redis command multiple times. For example :

func PollForData(conn redis.Conn) error {
	var url string
	var err error
	for {
		if url, err = conn.Do("LPOP", "URLS"); err != nil {
			return err
		}
		go func(input string) {
			// do something with the input
		}(url)
	}
	panic("Shouldn't be here")
}

To test it, you can chain redis responses. Let's write a test case

func TestPollForData(t *testing.T) {
  	redigomock.Clear()
  	redigomock.Command("LPOP", "URLS").Expect("www.some.url.com").Expect("www.another.url.com").ExpectError(redis.ErrNil)

  	err := PollForData(redigomock.NewConn())
	if err != redis.ErrNil {
		t.Error("This should return redis nil Error")
	}
}

In the first iteration of the loop redigomock would return "www.some.url.com", then "www.another.url.com" and finally redis.ErrNil

Sometimes providing expected arguments to redigomock at compile time could be too constraining. Let's imagine you use redis hash sets to store some data, along with the timestap of the last data update. Let's expand our Person struct :

type Person struct {
	Name      string `redis:"name"`
	Age       int    `redis:"age"`
	UpdatedAt uint64 `redis:updatedat`
	Phone     string `redis:phone`
}

And add a function updating personal data (phone number for example). Please notice that the update timestamp can't be determined at compile time

func UpdatePersonalData(conn redis.Conn, id string, person Person) error{
	_, err := conn.Do("HMSET", fmt.Sprint("person:", id), "name", person.Name, "age", person.Age, "updatedat" , time.Now.Unix(), "phone" , person.Phone)
	return err
}

Unit test :

func TestUpdatePersonalData(t *testing.T){
	redigomock.Clear()

	person := Person{
		Name :"A name",
		Age : 18
		Phone : "123456"
	}

	redigomock.Commmand("HMSET", "person:1", "name", person.Name, "age", person.Age, "updatedat", redigomock.NewAnyInt(), "phone", person.Phone).Expect("OK!")
	err := UpdatePersonalData(redigomock.NewConn(), "1", person)
	if err != nil {
		t.Error("This shouldn't return any errors")
	}
}

As you can see at the position of current timestamp redigomock is told to match AnyInt struct created by NewAnyInt() method. AnyInt struct will match any integer passed to redigomock from the tested method. Please see fuzzyMatch.go file for more details.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Clear

func Clear()

Clear removes all registered commands and empty the queue

func NewConn

func NewConn() redis.Conn

NewConn returns a new mocked connection. Obviously as we are mocking we don't need any Redis conneciton parameter

Types

type Cmd

type Cmd struct {
	Name      string        // Name of the command
	Args      []interface{} // Arguments of the command
	Responses []Response    // Slice of returned responses
}

Cmd stores the registered information about a command to return it later when request by a command execution

func Command

func Command(commandName string, args ...interface{}) *Cmd

Command register a command in the mock system using the same arguments of a Do or Send commands. It will return a registered command object where you can set the response or error

func GenericCommand

func GenericCommand(commandName string) *Cmd

GenericCommand register a command without arguments. If a command with arguments doesn't match with any registered command, it will look for generic commands before throwing an error

func Script

func Script(scriptData []byte, keyCount int, args ...interface{}) *Cmd

Script registers a command in the mock system just like Command method would do The first argument is a byte array with the script text, next ones are the ones you would pass to redis Script.Do() method

func (*Cmd) Expect

func (c *Cmd) Expect(response interface{}) *Cmd

Expect sets a response for this command. Everytime a Do or Receive methods are executed for a registered command this response or error will be returned. Expect call returns a pointer to Cmd struct, so you can chain Expect calls. Chained responses will be returned on subsequend calls matching this commands arguments in FIFO order.

func (*Cmd) ExpectError

func (c *Cmd) ExpectError(err error) *Cmd

ExpectError allows you to force an error when executing a command/arguments

func (*Cmd) ExpectMap

func (c *Cmd) ExpectMap(response map[string]string) *Cmd

ExpectMap works in the same way of the Expect command, but has a key/value input to make it easier to build test environments

type Conn

type Conn struct {
	ReceiveWait bool         // When set to true, Receive method will wait for a value in ReceiveNow channel to proceed, this is useful in a PubSub scenario
	ReceiveNow  chan bool    // Used to lock Receive method to simulate a PubSub scenario
	CloseMock   func() error // Mock the redigo Close method
	ErrMock     func() error // Mock the redigo Err method
	FlushMock   func() error // Mock the redigo Flush method
}

Conn is the struct that can be used where you inject the redigo.Conn on your project

func (Conn) Close

func (c Conn) Close() error

Close can be mocked using the Conn struct attributes

func (Conn) Do

func (c Conn) Do(commandName string, args ...interface{}) (reply interface{}, err error)

Do looks in the registered commands (via Command function) if someone matchs with the given command name and arguments, if so the corresponding response or error is returned. If no registered command is found an error is returned

func (Conn) Err

func (c Conn) Err() error

Err can be mocked using the Conn struct attributes

func (Conn) Flush

func (c Conn) Flush() error

Flush can be mocked using the Conn struct attributes

func (Conn) Receive

func (c Conn) Receive() (reply interface{}, err error)

Receive will process the queue created by the Send method, only one item of the queue is processed by Receive call. It will work as the Do method.

func (Conn) Send

func (c Conn) Send(commandName string, args ...interface{}) error

Send stores the command and arguments to be executed later (by the Receive function) in a first- come first-served order

type FuzzyMatcher

type FuzzyMatcher interface {

	//Func Match takes an argument passed to mock connection Do call and check if it fulfulls constraints set in concrete implementation of this interface
	Match(interface{}) bool
}

FuzzyMatcher is an interface that exports exports one function. It can be passed to the Command as a argument. When the command is evaluated agains data provided in mock connection Do call, FuzzyMatcher will call Match on the argument and returns true if argument fulfils constraints set in concrete implementation .

func NewAnyData

func NewAnyData() FuzzyMatcher

NewAnyData returns a FuzzyMatcher instance matching every data passed as an arguments (returnes true by default)

func NewAnyDouble

func NewAnyDouble() FuzzyMatcher

NewAnyDouble returns a FuzzyMatcher instance mathing any double passed as an argument

func NewAnyInt

func NewAnyInt() FuzzyMatcher

NewAnyInt retunrs a FuzzyMatcher instance maching any integer passed as an argument

type Response

type Response struct {
	Response interface{} // Response to send back when this command/arguments are called
	Error    error       // Error to send back when this command/arguments are called
}

Response struct that represents single response from `Do` call

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL