【Swift】RandomNumberGeneratorを用いてランダム系メソッドをテスタブルにする

# はじめに
Swift4.2にランダム系メソッドが充実して、乱数の生成やコレクションのシャッフル等が簡単に扱えるようになりました。
一方で、それらを用いた処理をテストしたい場合にシードを指定して出力を固定しないと満足にテストすることができません。
そこでランダム系メソッドをテスタブルにするために`RandomNumberGenerator`[^1]というものが用意されています。
本稿ではその`RandomNumberGenerator`の使い方について調べました。

# RandomNumberGeneratorとは
`RandomNumberGenerator`自体はプロトコルであり、これに準拠したclassまたはstructを定義し、ランダム系メソッドに渡すことでランダムを制御することができます。

```swift
struct SampleRNG: RandomNumberGenerator {
    func next() -> UInt64 {
        // 値を返す
    }
}

var sampleRNG = SampleRNG()
let value = Int.random(in: 0..<5, using: &sampleRNG)
let array = (0..<5).shuffled(using: &sampleRNG)
```

`using:`で指定しなかった場合のデフォルトには`SystemRandomNumberGenerator`[^2]が指定されます。
しかし、`SystemRandomNumberGenerator`は自動的にシードが決定されてしまうためテストでの使用には適していません。

# next()は何を返すべきか
では、自前でシードを指定できる`RandomNumberGenerator`を実装するとして、必ず定義しないといけない`next()`は何を返せばいいのでしょうか。

試しに`SystemRandomNumberGenerator`の`next()`を実行してみると

```swift
(0..<3).forEach { _ in
    var systemRNG = SystemRandomNumberGenerator()
    print((0..<5).map{ _ in systemRNG.next() })
}
/**
出力結果:
[16036380894180833889, 7700499849190268893, 10995696043040813552, 9668271037276668185, 3313056213622593400]
[8744019621750165094, 9260477925474910325, 17202348659366549963, 13714465276537005790, 4219747717231563950]
[13950181904023265219, 6955623721175513530, 16673547766684375502, 16897809642619452745, 14871188299320407218]
*/
```
`next()`自体が乱数を返すようです。
なので、シードを指定できる別の乱数生成手段を用いて乱数を返せば良さそうです。

調べてみるとSwiftでシードを指定して乱数生成する手段として、`srand48()`と`drand48()`を用いる方法と`GameplayKit`を用いる方法の2つがあるようです。[^3]
ゲームでないアプリで`GameplayKit`を用いるのは気が引けるので前者の方法をやってみました。

```swift
import Foundation

struct TestRandomNumberGenerator: RandomNumberGenerator {
    init(seed: Int) {
        srand48(seed)
    }
    
    mutating func next() -> UInt64 {
        return UInt64(drand48() * Double(UInt64.max))
    }
}

(0..<3).forEach { seed in
    print("seed = \(seed)")
    (0..<3).forEach { _ in
        var testRNG = TestRandomNumberGenerator(seed: seed)
        let output = (0..<5).map{ _ in Int.random(in: 0..<100, using: &testRNG) }
        print(output)
    }
}

/**
出力結果:
seed = 0
[17, 74, 9, 87, 57]
[17, 74, 9, 87, 57]
[17, 74, 9, 87, 57]
seed = 1
[4, 45, 83, 33, 56]
[4, 45, 83, 33, 56]
[4, 45, 83, 33, 56]
seed = 2
[91, 15, 57, 80, 55]
[91, 15, 57, 80, 55]
[91, 15, 57, 80, 55]
*/
```
うまく乱数がばらけているかは別として、シードを指定して乱数を固定化することができました。

# DIできる形にする
最後にアプリケーション実行時には普段通りに`SystemRandomNumberGenerator`を指定し、テスト実行時には自前で作成した`TestRandomNumberGenerator`を指定するように切り替えられるようにします。
`RandomNumberGenerator`がプロトコルであることを利用してDIする方法が考えられますが、ここで問題となるのがメソッドに渡すのが参照渡しとなっているところです。
以下のようにプロトコルを参照渡ししようとするとエラーになります。

```swift
class Hoge {
    var rng: RandomNumberGenerator
    
    init(rng: RandomNumberGenerator) {
        self.rng = rng
    }
    
    func exec() -> Int {
        return Int.random(in: 0..<100, using: &self.rng) // エラーになる
    }
}

let rng: RandomNumberGenerator = isTest ? TestRandomNumberGenerator(seed: 1) : SystemRandomNumberGenerator()
let hoge = Hoge(rng: rng)
print(hoge.exec())
```

解決策として`RandomNumberGenerator`のWrapperクラスを作成してプロトコルを内包するというのが考えられます。

```swift
struct RandomNumberGeneratorWrapper: RandomNumberGenerator {
    private var rng: RandomNumberGenerator
    
    init(rng: RandomNumberGenerator = SystemRandomNumberGenerator()) {
        self.rng = rng
    }
    
    mutating func next() -> UInt64 {
        return self.rng.next()
    }
}

class Hoge {
    var rngWrapper: RandomNumberGeneratorWrapper
    
    init(rngWrapper: RandomNumberGeneratorWrapper) {
        self.rngWrapper = rngWrapper
    }
    
    func exec() -> Int {
        return Int.random(in: 0..<100, using: &self.rngWrapper)
    }
}

let rngWrapper: RandomNumberGeneratorWrapper = isTest ? .init(rng: TestRandomNumberGenerator(seed: 1)) : .init()
let hoge = Hoge(rngWrapper: rngWrapper)
print(hoge.exec())
```

こちらの方法だとDIしながら問題なく参照渡しすることができます。

# おわりに
RandomNumberGeneratorを用いてランダム系メソッドをテスタブルにする方法について調べました。
テストを書く際の一助となれば幸いです。

[^1]: https://developer.apple.com/documentation/swift/randomnumbergenerator/
[^2]: https://developer.apple.com/documentation/swift/systemrandomnumbergenerator
[^3]: https://stackoverflow.com/questions/54821659/swift-4-2-seeding-a-random-number-generator