2019年12月27日金曜日

go-workers でワーカーをテストする方法を考える、その2

概要

前回 go-workers のテスト戦略を考えてみました
更に良い方法が見つかったのでコードを紹介します

環境

  • macOS 10.15.2
  • golang 1.12.9

main.go

変更点は Worker 構造体の関数として Perform を登録したのでそれを workers.Process に渡しています

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/main/main.go
package main

import (
    "github.com/jrallison/go-workers"
    "github.com/hawksnowlog/a/worker"
)

func main() {
    workers.Configure(map[string]string{
        "server":  worker.GetRedisURL(),
        "process": "1",
    })
    // ここにワーカーのメイン処理を指定します
    w := worker.NewWorker()
    // w が持つ Perform 関数をワーカーのメイン関数として設定します
    workers.Process("default", w.Perform, 1)
    workers.Run()
}

worker.go

先程紹介したように Worker 構造体の関数として Perform を定義しています
Perform 内で NewWorker を呼ばなくなり直接 Worker 構造体のフィールドにアクセスできるようになっています
こうすることで Worker 構造体のフィールドをモックにすることで Perform を直接呼び出してテストできるようにしています

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/worker.go
package worker

import (
    "fmt"
    "github.com/jrallison/go-workers"
)

// Worker 構造体は外部リソースを管理する構造体を持っています
// テスト時にモックしたいリソースは Interface として定義します (重要)
type Worker struct {
    logger *Logger
    Redis  RedisInterface
}

// Worker 構造体を生成します
func NewWorker() *Worker {
    return &Worker{
        logger: NewLogger(),
        Redis:  NewRedis(),
    }
}

// ワーカーのメイン処理になります
// ここで外部リソースにアクセスしたりします
// Worker 構造体の関数として定義することでテストしやすいコードにしています
func (w *Worker) Perform(msg *workers.Msg) {
    err := w.Redis.Open()
    if err != nil {
        w.logger.Info(err.Error())
        return
    }
    defer w.Redis.Close()
    a, _ := msg.Args().Array()
    w.logger.Info(fmt.Sprintf("%s", a[0]))
    err = w.Redis.Set(msg.Jid())
    if err != nil {
        w.logger.Info(err.Error())
        return
    }
    data, err := w.Redis.Get()
    if err != nil {
        w.logger.Info(err.Error())
        return
    }
    w.logger.Info(string(data))
}

redis.go

Perform の終了後に defer を使って Redis とのコネクションを Close しているので冒頭で Open できるように関数を追加しています

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/redis.go
package worker

import (
    "github.com/gomodule/redigo/redis"
    "os"
)

// モックするために Interface を定義します
type RedisInterface interface {
    Set(data string) error
    Get() ([]byte, error)
    Close()
    Open() error
}

type Redis struct {
    client redis.Conn
}

const keyName = "data"
const defaultRedisURL = "localhost:6379"

func NewRedis() *Redis {
    client, _ := redis.Dial("tcp", GetRedisURL())
    return &Redis{
        client: client,
    }
}

func GetRedisURL() string {
    if url, ok := os.LookupEnv("REDIS_URL"); ok {
        return url
    }
    return defaultRedisURL
}

func (r *Redis) Open() error {
    client, err := redis.Dial("tcp", GetRedisURL())
    if err != nil {
        return err
    }
    r.client = client
    return nil
}

func (r *Redis) Set(data string) error {
    _, err := r.client.Do("set", keyName, data)
    if err != nil {
        return err
    }
    return nil
}

func (r *Redis) Get() ([]byte, error) {
    data, err := redis.Bytes(r.client.Do("get", keyName))
    if err != nil {
        return nil, err
    }
    return data, nil
}

func (r *Redis) Close() {
    r.client.Close()
}

fake_redis.go

redis.go 同様に Open を追加しています

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/fake_redis.go
package worker

type FakeRedis struct {
}

const FakeJid = "054f5c637bc26dbd7491a758"

func NewFakeRedis() *FakeRedis {
    return &FakeRedis{}
}

func (r *FakeRedis) Open() error {
    return nil
}

func (r *FakeRedis) Set(data string) error {
    return nil
}

func (r *FakeRedis) Get() ([]byte, error) {
    return []byte(FakeJid), nil
}

func (r *FakeRedis) Close() {
}

worker_test.go

そしてテストを変更します
NewWorkerWorker を作成したらそのまま Perform を呼び出しています
その前に FakeRedis のモックを Worker フィールドに設定します
こうすることで Redis に直接アクセスすることなく Perform 関数内でモックを使ってくれます

  • vim $GOPATH/src/github.com/hawksnowlog/a/worker/worker_test.go
package worker

import (
    "fmt"
    "github.com/jrallison/go-workers"
    "testing"
)

func TestWorker_Perform(t *testing.T) {
    // キューイングされたメッセージを擬似的に生成
    msg, _ := workers.NewMsg(fmt.Sprintf(`{"jid":"%s","args":["1","2"]}`, FakeJid))
    mainWorker := NewWorker()
    // Redis には実際にアクセスしない FakeRedis を設定
    mainWorker.Redis = NewFakeRedis()
    mainWorker.Perform(msg)
}

最後に

おそらくこの方法が一番良い方法だと思います
テストのなので良い悪いとかはないと思うのですがソースコードもキレイでかつテストも書きやすいコードになるのでは思います

参考までに

0 件のコメント:

コメントを投稿