Deterministic Random Numbers in PHP

Testing code that leverages random numbers can be tricky. It's useful to have a way to subvert the random number generator during tests to yield a deterministic state.

Yesterday we talked about mocking time() in unit tests. It's a useful trick when trying to create a deterministic state in otherwise dynamic codes. Another situation where this can become important is when testing code that uses random numbers.

Random numbers are, by definition, random. If you can predict the numbers that will be generated by the function, then your implementation is broken and highly insecure. Last year, for example, a flaw in the random number generator (RNG) used by several hardware security chips was discovered that made it trivial to factor the private component of RSA keypairs.

Keeping random things truly random is a good idea.

Removing Randomness from Tests

Unfortunately, it's hard to set up a test expectation on code that uses an unpredictable, random state. So, instead, we're going to wrap our otherwise secure random number generator in an object that allows us to specify state at creation time.

_items = $seeds;
    $this->_position = 0;
  }

  public function __invoke(int $min, int $max)
  {
    if (! isset($this->_items[$this->_position])) {
      $item = $this->_items[$this->_position] = \random_int($min, $max);
    } else {
      $item = $this->_items[$this->_position];
    }

    ++$this->_position;

    return $item;
  }
}

This class takes in an optional array of generators numbers when it's instantiated. If you provide this array, then its elements will be returned to you in turn every time you use the object. If you don't provide the array, then new, truly random numbers will be generated on each invocation.

The class has a side benefit of storing the generated numbers as it goes. In some test situations, you might still want to generate real randomness but be able to reference back to what was generated. I won't cover that here, but you've got the building blocks you need to move forward.

Utilization

Once the object is instantiated, it can be used the same way as random_int() would be used otherwise:

$random = new RandomNumberGenerator();
var_dump($random(100, 999));
var_dump($random(-1000, 0));

The above example would produce something similar to:

int(248)
int(-898)

Use Under Test

As with any other test, the trick is to set things up before the code is invoked. If your code is using dependency injection, you can merely pass your custom random number generator in at runtime. It's more likely, though, that your code is invoking a global random_int() invocation. In that case, we can use a similar trick to the one we used to substitute time():

 $value) {
            $actual = random_int(0, 10);
            $this->assertEqual($actual, $value);
        }
    }
}

By default, invoking random_int() in a test will still return the result of a system call. However, if we need to override that behavior, we can set $generator to be an instance of our custom random number generator to produce deterministic values on each invocation. We could even set it by default, but without seeding the inner array, to still generate real random numbers (but keep track of them along the way).

This is a simple yet powerful trick to make RNG-based functionality in PHP easily testable.