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/gomodule/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

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

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

	if conn.Stats(cmd) != 1 {
		fmt.Println("Command was not used")
		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

	conn.Clear()
	cmd = conn.Command("HGETALL", "person:1").ExpectError(fmt.Errorf("Simulate error!"))

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

	if conn.Stats(cmd) != 1 {
		fmt.Println("Command was not used")
		return
	}

	fmt.Println("Success!")
}

mocking a subscription

package main

import "github.com/rafaeljusto/redigomock"

func CreateSubscriptionMessage(data []byte) []interface{} {
	values := []interface{}{}
	values = append(values, interface{}([]byte("message")))
	values = append(values, interface{}([]byte("chanName")))
	values = append(values, interface{}(data))
	return values
}

func main() {
	conn := redigomock.NewConn()

	// Setup the initial subscription message
	values := []interface{}{}
	values = append(values, interface{}([]byte("subscribe")))
	values = append(values, interface{}([]byte("chanName")))
	values = append(values, interface{}([]byte("1")))

	conn.Command("SUBSCRIBE", subKey).Expect(values)
	conn.ReceiveWait = true

	// Add a response that will come back as a subscription message
	conn.AddSubscriptionMessage(CreateSubscriptionMessage([]byte("hello")))

	// You need to send messages to conn.ReceiveNow in order to get a response.
	// Sending to this channel will block until receive, so do it in a goroutine
	go func() {
		conn.ReceiveNow <- true // This unlocks the subscribe message
		conn.ReceiveNow <- true // This sends the "hello" message
	}()
}

connections pool

// Note you cannot get access to the connection via the pool,
// the only way is to use this conn variable.
conn := redigomock.NewConn()
pool := &redis.Pool{
	// Return the same connection mock for each Get() call.
	Dial:    func() (redis.Conn, error) { return conn, nil },
	MaxIdle: 10,
}

dynamic handling arguments

Sometimes you need to check the executed arguments in your Redis command. For that you can use the command handler.

package main

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

func Publish(conn redis.Conn, x, y int) error {
	if x < 0 {
		x = 0
	}
	if y < 0 {
		y = 0
	}
	_, err := conn.Do("PUBLISH", "sumCh", int64(x+y))
	return err
}

func main() {
	conn := redigomock.NewConn()
	conn.GenericCommand("PUBLISH").Handle(redigomock.ResponseHandler(func(args []interface{}) (interface{}, error) {{
		if len(args) != 2 {
			return nil, fmt.Errorf("unexpected number of arguments: %d", len(args))
		}
		v, ok := args[1].(int64)
		if !ok {
			return nil, fmt.Errorf("unexpected type %T", args[1])
		}
		if v < 0 {
			return nil, fmt.Errorf("unexpected value '%d'", v)
		}
		return int64(1), nil
	})

	if err := Publish(conn, -1, 10); err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println("Success!")
}
Expand ▾ Collapse ▴

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.

    c.Command("HGETALL", "person:1").Expect("Person!")
    c.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:

    c.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.

    c.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).

    c.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.

    c.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/gomodule/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) {
      conn := redigomock.NewConn()
      cmd := conn.Command("HGETALL", "person:1").ExpectMap(map[string]string{
        "name": "Mr. Johson",
        "age":  "42",
      })
    
      person, err := RetrievePerson(conn, "1")
      if err != nil {
        t.Fatal(err)
      }
    
      if conn.Stats(cmd) != 1 {
        t.Fatal("Command was not called!")
      }
    
      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) {
      conn := redigomock.NewConn()
      conn.Command("HGETALL", "person:1").ExpectError(fmt.Errorf("Simulate error!"))
    
      person, err = RetrievePerson(conn, "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) {
      conn := redigomock.NewConn()
      conn.Command("LPOP", "URLS").
        Expect("www.some.url.com").
        Expect("www.another.url.com").
        ExpectError(redis.ErrNil)
    
      if err := PollForData(conn); 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 timestamp 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"
      }
    
      conn := redigomock.NewConn()
      conn.Commmand("HMSET", "person:1", "name", person.Name, "age", person.Age, "updatedat", redigomock.NewAnyInt(), "phone", person.Phone).Expect("OK!")
    
      err := UpdatePersonalData(conn, "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

    This section is empty.

    Types

    type Cmd

    type Cmd struct {
    	Name      string        // Name of the command
    	Args      []interface{} // Arguments of the command
    	Responses []Response    // Slice of returned responses
    	Called    bool          // State for this command called or not
    }

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

      func (*Cmd) Expect

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

        Expect sets a response for this command. Every time a Do or Receive method is 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 subsequent 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

            func (*Cmd) ExpectPanic

            func (c *Cmd) ExpectPanic(msg interface{}) *Cmd

              ExpectPanic allows you to force a panic when executing a command/arguments

              func (*Cmd) ExpectSlice

              func (c *Cmd) ExpectSlice(resp ...interface{}) *Cmd

                ExpectSlice makes it easier to expect slice value e.g - HMGET command

                func (*Cmd) ExpectStringSlice

                func (c *Cmd) ExpectStringSlice(resp ...string) *Cmd

                  ExpectStringSlice makes it easier to expect a slice of strings, plays nicely with redigo.Strings

                  func (*Cmd) Handle

                  func (c *Cmd) Handle(fn ResponseHandler) *Cmd

                    Handle registers a function to handle the incoming arguments, generating an on-the-fly response.

                    type Conn

                    type Conn struct {
                    	SubResponses       []Response   // Queue responses for PubSub
                    	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
                    	FlushSkippableMock func() error // Mock the redigo Flush method, will be ignore if return with a nil.
                    
                    	Errors []error // Storage of all error occured in do functions
                    	// contains filtered or unexported fields
                    }

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

                      func NewConn

                      func NewConn() *Conn

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

                        func (*Conn) AddSubscriptionMessage

                        func (c *Conn) AddSubscriptionMessage(msg interface{})

                          AddSubscriptionMessage register a response to be returned by the receive call.

                          func (*Conn) Clear

                          func (c *Conn) Clear()

                            Clear removes all registered commands. Useful for connection reuse in test scenarios

                            func (*Conn) Close

                            func (c *Conn) Close() error

                              Close can be mocked using the Conn struct attributes

                              func (*Conn) Command

                              func (c *Conn) 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 (*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 matches 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) DoWithTimeout

                                  func (c *Conn) DoWithTimeout(readTimeout time.Duration, cmd string, args ...interface{}) (interface{}, error)

                                    DoWithTimeout is a helper function for Do call to satisfy the ConnWithTimeout interface.

                                    func (*Conn) Err

                                    func (c *Conn) Err() error

                                      Err can be mocked using the Conn struct attributes

                                      func (*Conn) ExpectationsWereMet

                                      func (c *Conn) ExpectationsWereMet() error

                                        ExpectationsWereMet can guarantee that all commands that was set on unit tests called or call of unregistered command can be caught here too

                                        func (*Conn) Flush

                                        func (c *Conn) Flush() error

                                          Flush can be mocked using the Conn struct attributes

                                          func (*Conn) GenericCommand

                                          func (c *Conn) 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 (*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) ReceiveWithTimeout

                                              func (c *Conn) ReceiveWithTimeout(timeout time.Duration) (interface{}, error)

                                                ReceiveWithTimeout is a helper function for Receive call to satisfy the ConnWithTimeout interface.

                                                func (*Conn) Script

                                                func (c *Conn) 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 (*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

                                                    func (*Conn) Stats

                                                    func (c *Conn) Stats(cmd *Cmd) int

                                                      Stats returns the number of times that a command was called in the current connection

                                                      type FuzzyMatcher

                                                      type FuzzyMatcher interface {
                                                      
                                                      	// Match takes an argument passed to mock connection Do call and checks if
                                                      	// it fulfills constraints set in concrete implementation of this interface
                                                      	Match(interface{}) bool
                                                      }

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

                                                        func NewAnyData

                                                        func NewAnyData() FuzzyMatcher

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

                                                          func NewAnyDouble

                                                          func NewAnyDouble() FuzzyMatcher

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

                                                            func NewAnyInt

                                                            func NewAnyInt() FuzzyMatcher

                                                              NewAnyInt returns a FuzzyMatcher instance matching 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
                                                              	Panic    interface{} // Panic to throw when this command/arguments are called
                                                              }

                                                                Response struct that represents single response from `Do` call

                                                                type ResponseHandler

                                                                type ResponseHandler func(args []interface{}) (interface{}, error)

                                                                  ResponseHandler dynamic handles the response for the provided arguments.