fkm blog

software開発に関することを書いていきます

乱数は外部で生成されるものとして扱う

例えばユーザーIDは乱数で決める仕様だったとする。 UUID v4でお手軽に生成し、万が一ぶつかったらリトライすることにする。

こんな感じのコードになると思う。

func InsertUser(db *sql.DB, email, name string) {
    st, err := db.Prepare("insert into user(id, email, name) values(?, ?, ?)")
    if err != nil {
        return
    }
    defer st.Close()

    for i := 0 ; i < 5 ; i++ {
        uid := uuid.New() // 乱数で生成
        _, err = st.Exec(uid, email, name)
        if err == nil {
            return
        }
    }
    log.Printf("Failed to insert user")
}

でもこれだと、テストの時に毎回ランダムでuidが生成されるので、テストしづらい。

問題となる部分は以下の部分。

uid := uuid.New() // 乱数で生成

乱数生成を、「外部で生成されるもの」と捉えると、次のようなインターフェースが作れる。

type Generator interface {
    Generate() string
}

呼ぶと乱数使ってuidを生成してくれるインターフェース。で、これを使うよう先ほどの関数を書き換えると、こんな感じ。

func InsertUser(db *sql.DB, uidGenerator Generator, email, name string) {
    st, err := db.Prepare("insert into user(id, email, name) values(?, ?, ?)")
    if err != nil {
        return
    }
    defer st.Close()

    for i := 0 ; i < 5 ; i++ {
        uid := uidGenerator.Generate() // インターフェースとしてもらって、呼ぶ
        _, err = st.Exec(uid, email, name)
        if err == nil {
            return
        }
    }
    log.Printf("Failed to insert user")
}

こうすると、テスト時には次のように固定のuidを返すモック実装に差し替えれる。

type mockGenerator struct { }

func (m *mockGenerator) Generate() string {
    return "mockUID"
}

func Test_Insert(t *testing.T) {
    db := ....
    email := "demo@example.com"
    name := "demo"

    InsertUser(db, &mockGenerator{}, email, name)
    // アサーションを忘れずに
}

乱数は外部で生成されるものとして、扱うようにしよう。