はじめに
Swift4.2にランダム系メソッドが充実して、乱数の生成やコレクションのシャッフル等が簡単に扱えるようになりました。
一方で、それらを用いた処理をテストしたい場合にシードを指定して出力を固定しないと満足にテストすることができません。
そこでランダム系メソッドをテスタブルにするためにRandomNumberGenerator
1というものが用意されています。
本稿ではそのRandomNumberGenerator
の使い方について調べました。
RandomNumberGeneratorとは
RandomNumberGenerator
自体はプロトコルであり、これに準拠したclassまたはstructを定義し、ランダム系メソッドに渡すことでランダムを制御することができます。
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()
を実行してみると
(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
を用いるのは気が引けるので前者の方法をやってみました。
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する方法が考えられますが、ここで問題となるのがメソッドに渡すのが参照渡しとなっているところです。
以下のようにプロトコルを参照渡ししようとするとエラーになります。
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クラスを作成してプロトコルを内包するというのが考えられます。
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を用いてランダム系メソッドをテスタブルにする方法について調べました。
テストを書く際の一助となれば幸いです。