redis/go-redis

Question: Testing Best Prcatice

krak3n opened this issue ยท 4 comments

Hey

First off this is an amazing library and I love it ๐Ÿ‘

This is a generate question about how I might go about writing unittest's in my application which uses this library. For example, best practice for testing errors potentially mocking results etc.

I'm finding it hard, without writing a wrapper around this lib defining my own interfaces to come up with a clean way for testing my application handles errors etc correctly from this library.

Anyway, any pointers in the right direction would be greatly appreciated ๐Ÿ˜„

Thanks,

Chris

dim commented

I'm not sure if it helps, but take a look at https://github.com/bsm/multiredis, it defines a commands interface. Alternatively, you can also look into https://github.com/alicebob/miniredis.

Ultimately, it's probably best practice to wrap the redis client in you own struct, test that against an actual redis database and then expose that struct to the rest of your app via an interface:

// DB is a wrapper interface, stub it out in tests of other packages
type DB interface {
  // GetData is the only method it supports
  GetData(id int64) ([]int64, error)
  // Exposing Close is a really good idea
  Close() error 
}

// The implementation, using an actual redis client.
// Should be tested against an actual redis server instance or (at least) miniredis
type dbImpl struct {
  *redis.Client
}

// Constructor, returns a DB
func Connect(opt *redis.Options) DB {
  return &dbImpl{Client: redis.NewClient(opt)}
}

// Implementation if the interface method
func (db *dbImpl) GetData(id int64) ([]int64, error) {
  key := fmt.Sprintf("resource:%d", id)
  strs, err := db.Smembers(key).Result()
  if err != nil {
    return nil, err
  }
  nums := make([]int64, len(strs))
  for i, s := range strs {
    num, err := strconv.ParseInt(s, 10, 64)
    if err != nil {
      return nil, err
    }
    nums[i] = num
  }
}

Thank you the pointers @dim

I have ended up writing a simple wrapper interface like this:

type RedisClient interface {
    Get(string) (string, error)
    Set(string, string, time.Duration) (string, error)
    ZAdd(string, ...redis.Z) (int64, error)
    ZRem(string, ...string) (int64, error)
    Close() error
}

type Redis struct {
    c *redis.Client
}

func (r *Redis) Get(key string) (string, error) {
    return r.c.Get(key).Result()
}

func (r *Redis) Set(key string, value string, expr time.Duration) (string, error) {
    return r.c.Set(key, value, expr).Result()
}

// Other methods here

Allowing me to write a fake mockable redis interface in unitests like this:

type FakeRedis struct {
    GetFunc   func(string) (string, error)
    SetFunc   func(string, string, time.Duration) (string, error)
    ZAddFunc  func(string, ...redis.Z) (int64, error)
    ZRemFunc  func(string, ...string) (int64, error)
    CloseFunc func() error
}

func (r *FakeRedis) Get(key string) (string, error) {
    if r.GetFunc != nil {
        return r.GetFunc(key)
    }

    return "", fmt.Errorf("Get %s Error", key)
}

// Other methods here

Allowing my test cases to look like this:

func TestWhatEver(t *testing.T) {
    cases := []struct {
        redisClient db.RedisClient
        item        string
        err         error
    }{
        {
            &testutil.FakeRedis{},
            "foo",
            errors.New("Get key Error"),
        },
    }

    // Loop over cases here
}

So I can basically swap in and out real and fake redis clients as and when needed ๐Ÿ˜„

Zombie post, but this is pretty high on google. You can embed an interface inside a struct. Calling any methods from that interface will panic, however, you can then define your own copy of that method. This lets you avoid writing a wrapper interface.

type MyRedis struct {
    redis.Cmdable
}

func (mr *MyRedis) Set(key string, val interface{}, ttl time.Duration) *redis.StatusCmd {
    // your implementation
}

This struct will work in place of a redis client now, but only for Set calls.