Edited at
Go3Day 1

redisを扱うコードをユニットテストする #golang


はじめに

この記事は、Go3 AdventCalendarの1日目の記事です。

RedisやMySQLを扱うような技術的実装については、interfaceで抽象化するといった使用コード側のユニットテストノウハウはよく見られますが、技術的実装自体をテストする方法はノウハウがあまり見られないなと思いますので、今回はそういう場合のテストの方法について紹介できればと思います。

なお、今回紹介するコードは、https://github.com/hgsgtk/go-snippets/pull/16 にて公開しています。

.

├── docker-compose.yml
├── go.mod
├── go.sum
├── main.go
├── persistence
│   └── kvs
│   ├── client.go
│   └── client_test.go
└── testhelper
└── mock_redis_client.go

今回は、redis-client実装にあたり、次のライブラリを利用しています。

https://github.com/go-redis/redis


redisを扱う実装をする

詳細な説明はコード内に記載いたしますが、トークンで値を取得するような実装を例として考えていきます。


persistence/kvs/client.go

package kvs

import (
"strconv"
"time"

"github.com/go-redis/redis"
"github.com/pkg/errors"
)

// 値取得の際になかった場合はredis.Nilがerror型として返ってくる
// エラーハンドリングのためinternalなpackageに定義する
const Nil = redis.Nil

// New create instance of redis.Client
func New(dsn string) (*redis.Client, error) {
client := redis.NewClient(&redis.Options{
Addr: dsn,
Password: "",
DB: 0,
})
if err := client.Ping().Err(); err != nil {
return nil, errors.Wrapf(err, "failed to ping redis server")
}
return client, nil
}

// 今回はトークンベースで値を取得するような例で実装してきます。
const (
tokenKey = "TOKEN_"
tokenDuration = time.Second * 3600
)

// SetToken set token and value
func SetToken(cli *redis.Client, token string, userID int) error {
if err := cli.Set(tokenKey+token, userID, tokenDuration).Err(); err != nil {
return err
}
return nil
}

// SetToken set token and value
func GetIDByToken(cli *redis.Client, token string) (int, error) {
v, err := cli.Get(tokenKey+token).Result()
if err != nil {
return 0, errors.Wrapf(err, "failed to get id from redis by token")
}
id, err := strconv.Atoi(v)
if err != nil {
return 0, errors.Wrapf(err, "failed to convert string to int")
}
return id, nil
}



ユニットテストする

上記のコードをユニットテストするには大きく2つ手段があるかと思います。



  1. *redis.Clientをwrapしてinterfaceを作成しモック差し替え可能とする

  2. ユニットテスト用のredis serverを用意する

選択肢1を選択する場合の例として、go-redis/redis のissueで紹介されています。

https://github.com/go-redis/redis/issues/332

今回は、選択肢2をやっていきます。それにあたって、次のライブラリを使用します。

https://github.com/alicebob/miniredis

miniredisは、ユニットテスト用のredisモックサーバを作れるライブラリです。これを使ってテストを書いていきます。


テスト用redisサーバ

テスト用redisサーバを用意するテストヘルパーを作っていきます。念の為の説明ですが、テストヘルパーを作るには、*testing.Tを渡した上で、t.Helper()と書きます。テストヘルパーとして作ることで、これを使用したテストのエラー箇所がわかりやすくなります。


testhelper/mock_redis_client.go

package testhelper

import (
"testing"

"github.com/alicebob/miniredis"
"github.com/go-redis/redis"
)

func NewMockRedis(t *testing.T) *redis.Client {
t.Helper()

// redisサーバを作る
s, err := miniredis.Run()
if err != nil {
t.Fatalf("unexpected error while createing test redis server '%#v'", err)
}
// *redis.Clientを用意
client := redis.NewClient(&redis.Options{
Addr: s.Addr(),
Password: "",
DB: 0,
})
return client
}



テストをする

上で作成したテストヘルパーを使って、テストを書きましょう。モックサーバに対する*redis.Clientを用意してそれをテスト対象の関数に渡せば完了です。


persistence/kvs/client_test.go

package kvs_test

import (
"testing"
"time"

"github.com/higasgt/go-snippets/redis-cli/persistence/kvs"
"github.com/higasgt/go-snippets/redis-cli/testhelper"
)

func TestSetToken(t *testing.T) {
client := testhelper.NewMockRedis(t)

if err := kvs.SetToken(client, "test", 1); err != nil {
t.Fatalf("unexpected error while SetToken '%#v'", err)
}
actual, err := client.Get("TOKEN_test").Result()
if err != nil {
t.Fatalf("unexpected error while get value '%#v'", err)
}

if expected := "1"; expected != actual {
t.Errorf("expected value '%s', actual value '%s'", expected, actual)
}
}

func TestGetIDByToken(t *testing.T) {
client := testhelper.NewMockRedis(t)
if err := client.Set("TOKEN_test", 1, time.Second*1000).Err(); err != nil {
t.Fatalf("unexpected error while set test data '%#v'", err)
}

actual, err := kvs.GetIDByToken(client, "test")
if err != nil {
t.Fatalf("unexpected error while GetIDByToken '%#v'", err)
}
if expected := 1; expected != actual {
t.Errorf("expected value '%#v', actual value '%#v'", expected, actual)
}
}


これで、Redisを扱っているコードのテストを書くことができました。


おわりに

今回は、redisを扱うコード部分の実装を行い、それに対してテストを書く方法を紹介しました。

明日は、@takochuu さんです!