Reputation: 864
I'd like to inject a RandomNumberGenerator
to my class in order to write unit tests. However, it seems that the methods that receive a random number generator only work with concrete types.
Which means that
var rng: RandomNumberGenerator = SystemRandomNumberGenerator()
Bool.random(using: &rng)
does not compile, with the error:
In argument type
inout RandomNumberGenerator
,RandomNumberGenerator
does not conform to expected typeRandomNumberGenerator
but
var rng = SystemRandomNumberGenerator()
Bool.random(using: &rng)
does.
The trouble with that is that I'd like to use the default random number generator when running my app, and use a custom random number generator that i control for testing. What is the general approach to controlling randomness in order to make testable code in Swift?
Upvotes: 3
Views: 1014
Reputation: 9880
From this protocol-doesnt-conform-to-itself, I used the "Build a type eraser" answer
struct AnyRandomNumberGenerator: RandomNumberGenerator {
private var generator: RandomNumberGenerator
init(_ generator: RandomNumberGenerator) {
self.generator = generator
}
mutating func next() -> UInt64 {
return self.generator.next()
}
public mutating func next<T>() -> T where T : FixedWidthInteger, T : UnsignedInteger {
return self.generator.next()
}
public mutating func next<T>(upperBound: T) -> T where T : FixedWidthInteger, T : UnsignedInteger {
return self.generator.next(upperBound: upperBound)
}
}
then it can be used like this
var randomNumberGenerator: RandomNumberGenerator = SystemRandomNumberGenerator()
var random = AnyRandomNumberGenerator(randomNumberGenerator)
UInt8.random(in: .min ... .max, using: &random)
Upvotes: 4
Reputation: 864
In case someone has this problem in the future. I ended up writing my own RandomNumberGenerator
that used a SystemRandomNumberGenerator
in production and returns test values (if required) in debug mode. I don't know if this is the best way to go about it.
class TestableRNG: RandomNumberGenerator {
private var rng = SystemRandomNumberGenerator()
private var testValue: UInt64?
init() {
self.testValue = nil
}
/**
* - warning: Should only be used for testing purposes
*/
init(valueForTesting: UInt64) {
#if DEBUG
self.testValue = valueForTesting
#else
self.testValue = nil
#endif
}
func set(testValue: UInt64) {
#if DEBUG
self.testValue = testValue
#endif
}
func next() -> UInt64 {
#if DEBUG
if let testValue = testValue {
return testValue
} else {
return rng.next()
}
#else
return rng.next()
#endif
}
}
Upvotes: 0