Determinism is the philosophical position that for every event there exist conditions that could cause it.
Wikipedia
(haha…)
(…) A deterministic system is a system in which no randomness is involved in the development of future states of the system.
Wikipedia
Anything that is based on programmatically unpredictable behavior is considered non-deterministic.
Pseudo-random number generators (PRNG)
are deterministic.
CloudFlare lava lamps wall, "Lavarand"
class Calculator
{
public function add(float $a, float $b): float
{
return $a + $b;
}
}
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
public function testAdd(): void
{
$math = new Calculator();
$result = $math->add(1, 2);
static::assertSame(3, $result);
}
}
class DiceRoller
{
public function roll(
int $numberOfDice = 1,
int $diceSides,
int $bonus = 0
): int {
$result = $bonus;
for ($i = 0; $i < $numberOfDice; ++$i) {
$result += random_int(1, $diceSides);
}
return $result;
}
}
// Roll 2d6+3
$diceRoller->roll(2, 6, 3);
Solution 1:
Testing a lot of cases.
namespace Tests\App;
use PHPUnit\Framework\TestCase;
class DiceRollerTest extends TestCase
{
public function provide dice rolls(): \Generator
{
$sidesToTest = [4, 6, 8, 12, 20]; // d4, d6, etc.
$numberOfDicesToTest = range(1, 10); // Up to 10 dices at the same time.
$bonuses = [1, 2, 3, 4, 5]; // Not too much, that's already a lot.
foreach ($sidesToTest as $diceSides) {
foreach ($numberOfDicesToTest as $numberOfDice) {
foreach ($bonuses as $bonus) {
yield "$diceSides-$numberOfDice-$bonus" => [$diceSides, $numberOfDice, $bonus];
}
}
}
}
}
namespace Tests\App;
use PHPUnit\Framework\TestCase;
class DiceRollerTest extends TestCase
{
// ...
/** @dataProvider provide dice rolls */
public function test dice roller result is in dice range(
int $numberOfDice = 1,
int $diceSides, int $bonus = 0
): void {
$diceRoller = new DiceRoller();
$result = $diceRoller->roll($sides, $multiplier, $offset);
self::assertGreaterThanOrEqual(1 * $multiplier + $offset, $result);
self::assertLessThanOrEqual($sides * $multiplier + $offset, $result);
}
}
Pros:
Cons:
random_int() is not deterministic.
$diceRoller = new App\DiceRoller();
$count = 1000000;
$results = [];
for ($i = 1; $i <= $count; $i++) {
$results[] = $diceRoller->roll(2, 6, 3);
}
// Average value
echo array_sum($results) / $count, "\n";
$ for i in {1..10}; do php roll.php; done
11.998764
11.997870
12.003618
12.000348
11.999262
11.998068
11.993424
12.000720
12.003618
12.000378
Solution 2:
Override the random_int() function
in our tests
Don't do it.
Solution 3:
Create mocks
interface RandomIntProviderInterface
{
public function randomInt(int $min, int $max): int;
}
class DiceRoller
{
public function roll(
int $numberOfDice = 1,
int $diceSides,
int $bonus = 0
): int {
$result = $bonus;
for ($i = 0; $i < $numberOfDice; ++$i) {
$result += random_int(1, $diceSides);
}
return $result;
}
}
class DiceRoller
{
public function __construct(
private RandomIntProviderInterface $randomIntProvider
) {
}
public function roll(
int $numberOfDice = 1,
int $diceSides,
int $bonus = 0
): int {
$result = $bonus;
for ($i = 0; $i < $numberOfDice; ++$i) {
$result += $this->randomIntProvider->randomInt(1, $diceSides);
}
return $result;
}
}
class NativeRandomIntProvider implements RandomIntProviderInterface
{
public function randomInt(int $min, int $max): int
{
return \random_int($min, $max);
}
}
class DeterministicRandomIntProvider implements RandomIntProviderInterface
{
public int $determinedResult = 0;
public function randomInt(int $min, int $max): int
{
return $this->determinedResult;
}
}
class DiceRollerTest extends TestCase
{
public function test dice roller result(): void {
$randomIntProvider = new DeterministicRandomIntProvider();
$diceRoller = new DiceRoller($randomIntProvider);
$randomIntProvider->determinedResult = 1;
$result = $diceRoller->roll(2, 6, 3); // 2d6+3
self::assertSame(5, $result); // Yay!
}
}
@pierstoval
Freelance dev, architect, coach & trainer @Orbitaleio