Augusto Dias Noronha
Augusto Dias Noronha

Reputation: 864

How to control randomness for unit testing purposes in Swift?

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 type RandomNumberGenerator

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

Answers (2)

CiNN
CiNN

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

Augusto Dias Noronha
Augusto Dias Noronha

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

Related Questions