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
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 hereAllowing 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 hereAllowing 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 ๐
I use interfaces in my libs too, e.g. https://github.com/go-redis/rate/blob/v4/rate.go#L14-L16 and https://github.com/go-redis/cache/blob/v3/cache.go#L18-L22
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.