LoginSignup
4
2

More than 1 year has passed since last update.

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

Posted at

はじめに

Swift4.2にランダム系メソッドが充実して、乱数の生成やコレクションのシャッフル等が簡単に扱えるようになりました。
一方で、それらを用いた処理をテストしたい場合にシードを指定して出力を固定しないと満足にテストすることができません。
そこでランダム系メソッドをテスタブルにするためにRandomNumberGenerator1というものが用意されています。
本稿ではその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:で指定しなかった場合のデフォルトにはSystemRandomNumberGenerator2が指定されます。
しかし、SystemRandomNumberGeneratorは自動的にシードが決定されてしまうためテストでの使用には適していません。

next()は何を返すべきか

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

試しにSystemRandomNumberGeneratornext()を実行してみると

(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を用いてランダム系メソッドをテスタブルにする方法について調べました。
テストを書く際の一助となれば幸いです。

  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

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2