🥳

MySQLを扱うPHPのテストのためのLiteralMockerクラス

12 min read

目的

テストでは任意のリテラルを変数に与えたいというケースがある。そういったとき、逐一以下のように指定するのは煩わしい。

$userId   = 997;
$userName = 'Jane Doe';

...

そこで、以下のように記述できるような無作為な値を生成する Util クラスを用意して、よりテストの抽象度を高められるようにする。

$userId   = $this->m->int();
$userName = $this->m->varchar();

デメリットとしては、普段使いするとかなり頻繁にコールされると思われるため、塵が積もってテストの総実行時間に知覚できるような影響を及ぼす可能性がある。

コード

‼️ 以下のコードはちゃんとテストしていないので動かない箇所があるかもです。自分が実際使う機会があったら動作確認したりする予定です

LiteralMocker
<?php declare(strict_types=1);

namespace Test\Util;

/**
 * @package Test\Util
 */
final class LiteralMocker
{
	/**
	 * 生成して返した数値を保存しておく配列
	 * @var int[]
	 */
	private array $generatedIntList = [];

	/**
	 * 生成して返した文字列を保存しておく配列
	 * @var string[]
	 */
	private array $generatedStrList = [];

	/**
	 * 重複しない TINYINT 型の乱数を返す
	 *
	 * @param bool $isUnsigned
	 * @param int  $length
	 * @return int
	 */
	final public function tinyInt(bool $isUnsigned = true, int $length = 4): int
	{
		$invoker = function () use ($isUnsigned, $length): int {
			return RandomNumberGenerator::tinyInt($isUnsigned, $length);
		};
		return $this->getUniqIntAndMemorize($invoker);
	}

	/**
	 * 重複しない SMALLINT 型の乱数を返す
	 *
	 * @param bool $isUnsigned
	 * @param int  $length
	 * @return int
	 */
	final public function smallInt(bool $isUnsigned = true, int $length = 6): int
	{
		$invoker = function () use ($isUnsigned, $length): int {
			return RandomNumberGenerator::smallInt($isUnsigned, $length);
		};
		return $this->getUniqIntAndMemorize($invoker);
	}

	/**
	 * 重複しない MEDIUMINT 型の乱数を返す
	 *
	 * @param bool $isUnsigned
	 * @param int  $length
	 * @return int
	 */
	final public function mediumInt(bool $isUnsigned = true, int $length = 8): int
	{
		$invoker = function () use ($isUnsigned, $length): int {
			return RandomNumberGenerator::mediumInt($isUnsigned, $length);
		};
		return $this->getUniqIntAndMemorize($invoker);
	}

	/**
	 * 重複しない INT 型の乱数を返す
	 * PHP_INT_MAX, PHP_INT_MIN で丸められることがあるため注意
	 *
	 * @param bool $isUnsigned
	 * @param int  $length
	 * @return int
	 */
	final public function int(bool $isUnsigned = true, int $length = 11): int
	{
		$invoker = function () use ($isUnsigned, $length): int {
			return RandomNumberGenerator::int($isUnsigned, $length);
		};
		return $this->getUniqIntAndMemorize($invoker);
	}

	/**
	 * 重複しない BIGINT 型の乱数を返す
	 * PHP_INT_MAX, PHP_INT_MIN で丸められることがあるため注意
	 *
	 * @param bool $isUnsigned
	 * @param int  $length
	 * @return int
	 */
	final public function bigInt(bool $isUnsigned = true, int $length = 20): int
	{
		$invoker = function () use ($isUnsigned, $length): int {
			return RandomNumberGenerator::bigInt($isUnsigned, $length);
		};
		return $this->getUniqIntAndMemorize($invoker);
	}

	/**
	 * 重複しない CHAR 型のランダムな文字列を返す
	 *
	 * @param int    $length
	 * @param string $keyspace
	 * @return string
	 */
	final public function char(
		int $length = 16,
		string $keyspace = RandomStringGenerator::KEY_SPACE
	): string {
		$invoker = function () use ($length, $keyspace): string {
			return RandomStringGenerator::char($length, $keyspace);
		};
		return $this->getUniqStrAndMemorize($invoker);
	}

	/**
	 * 重複しない VARCHAR 型のランダムな文字列を返す
	 *
	 * @param bool   $isEmptyAllowed
	 * @param int    $length
	 * @param string $keyspace
	 * @return string
	 */
	final public function varchar(
		bool $isEmptyAllowed = true,
		int $length = 16,
		string $keyspace = RandomStringGenerator::KEY_SPACE
	): string {
		$invoker = function () use ($isEmptyAllowed, $length, $keyspace): string {
			return RandomStringGenerator::varchar($isEmptyAllowed, $length, $keyspace);
		};
		return $this->getUniqStrAndMemorize($invoker);
	}

	/**
	 * Mocker::varchar() のエイリアス
	 *
	 * @param bool   $isEmptyAllowed
	 * @param int    $length
	 * @param string $keyspace
	 * @return string
	 * @see Mocker::varchar()
	 */
	final public function text(
		bool $isEmptyAllowed = true,
		int $length = 16,
		string $keyspace = RandomStringGenerator::KEY_SPACE
	): string {
		return $this->varchar($isEmptyAllowed, $length, $keyspace);
	}

	/**
	 * ランダムな DATETIME 型の文字列を返す
	 *
	 * @param int    $range 現時点を基準とした秒数の幅
	 *                      デフォルトは 157680000 ( 365 日 x 5 した秒数 )
	 * @param string $format
	 * @return string
	 */
	final public function dateTime(
		int $range = 157680000,
		string $format = \DateTimeInterface::RFC3339
	): string {
		$sec = RandomNumberGenerator::randNum("-{$range}", (string)$range);
		return (new \DateTimeImmutable())
			->modify(sprintf('%s second', $sec))
			->format($format);
	}

	/**
	 * ランダムな BOOL 型の値を返す
	 *
	 * @return bool
	 */
	final public function bool(): bool
	{
		return '0' === RandomNumberGenerator::randNum('0', '1');
	}

	/**
	 * 既出の数値でなくなるまで $invoker を実行して、
	 * 結果の値をプロパティに保存しつつ返す
	 *
	 * @param callable $invoker fn() => int
	 * @return int
	 */
	private function getUniqIntAndMemorize(callable $invoker): int
	{
		do {
			$generated = $invoker();
		} while (in_array($generated, $this->generatedIntList, true));
		$this->generatedIntList[] = $generated;

		return $generated;
	}

	/**
	 * 既出の文字列でなくなるまで $invoker を実行して、
	 * 結果の値をプロパティに保存しつつ返す
	 *
	 * @param callable $invoker fn() => string
	 * @return string
	 */
	private function getUniqStrAndMemorize(callable $invoker): string
	{
		do {
			$generated = $invoker();
		} while (in_array($generated, $this->generatedStrList, true));
		$this->generatedStrList[] = $generated;

		return $generated;
	}
}
RandomStringGenerator
<?php declare(strict_types=1);

namespace Test\Util;

/**
 * @package Test\Util
 */
final class RandomStringGenerator
{
	/**
	 * 生成されるランダムな文字列に使用され得る文字
	 * @var string
	 */
	public const KEY_SPACE = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

	/**
	 * CHAR 型のランダムな文字列を返す
	 *
	 * @param int    $length
	 * @param string $keyspace
	 * @return string
	 */
	final public static function char(
		int $length = 16,
		string $keyspace = self::KEY_SPACE
	): string {
		return self::randStr($length, $keyspace);
	}

	/**
	 * VARCHAR 型のランダムな文字列を返す
	 *
	 * @param bool   $isEmptyAllowed
	 * @param int    $length
	 * @param string $keyspace
	 * @return string
	 */
	final public static function varchar(
		bool $isEmptyAllowed = true,
		int $length = 16,
		string $keyspace = self::KEY_SPACE
	): string {
		if ($isEmptyAllowed) {
			$lengthDetermined = RandomNumberGenerator::randNum('0', (string)$length);
			if ('0' === $lengthDetermined) {
				return '';
			}
		} else {
			$lengthDetermined = RandomNumberGenerator::randNum('1', (string)$length);
		}
		return self::randStr((int)$lengthDetermined, $keyspace);
	}

	/**
	 * @see https://stackoverflow.com/a/31107425
	 * @param int    $length
	 * @param string $keyspace
	 * @return string
	 */
	final public static function randStr(
		int $length,
		string $keyspace = self::KEY_SPACE
	): string {
		if ($length < 1) {
			throw new \RangeException('指定された文字列の長さが正の数でない');
		}

		$pieces = [];
		$max = mb_strlen($keyspace, '8bit') - 1;

		for ($i = 0; $i < $length; ++$i) {
			$pieces [] = $keyspace[random_int(0, $max)];
		}

		return implode('', $pieces);
	}
}
RandomNumberGenerator
<?php declare(strict_types=1);

namespace Test\Util;

use phpseclib3\Math\BigInteger;

/**
 * @package Test\Util
 */
final class RandomNumberGenerator
{
	/**
	 * TINYINT 型の乱数を返す
	 *
	 * @param bool $isUnsigned
	 * @param int  $length
	 * @return int
	 */
	final public static function tinyInt(bool $isUnsigned = true, int $length = 4): int
	{
		return self::randAnyIntType($isUnsigned, 8, $length);
	}

	/**
	 * SMALLINT 型の乱数を返す
	 *
	 * @param bool $isUnsigned
	 * @param int  $length
	 * @return int
	 */
	final public static function smallInt(bool $isUnsigned = true, int $length = 6): int
	{
		return self::randAnyIntType($isUnsigned, 16, $length);
	}

	/**
	 * MEDIUMINT 型の乱数を返す
	 *
	 * @param bool $isUnsigned
	 * @param int  $length
	 * @return int
	 */
	final public static function mediumInt(bool $isUnsigned = true, int $length = 8): int
	{
		return self::randAnyIntType($isUnsigned, 24, $length);
	}

	/**
	 * INT 型の乱数を返す
	 * PHP_INT_MAX, PHP_INT_MIN で丸められることがあるため注意
	 *
	 * @param bool $isUnsigned
	 * @param int  $length
	 * @return int
	 */
	final public static function int(bool $isUnsigned = true, int $length = 11): int
	{
		return self::randAnyIntType($isUnsigned, 32, $length);
	}

	/**
	 * BIGINT 型の乱数を返す
	 * PHP_INT_MAX, PHP_INT_MIN で丸められることがあるため注意
	 *
	 * @param bool $isUnsigned
	 * @param int  $length
	 * @return int
	 */
	final public static function bigInt(bool $isUnsigned = true, int $length = 20): int
	{
		return self::randAnyIntType($isUnsigned, 64, $length);
	}

	/**
	 * 引数に従う整数型の乱数を返す
	 * PHP_INT_MAX, PHP_INT_MIN で丸められることがあるため注意
	 *
	 * @param bool $isUnsigned
	 * @param int  $pow
	 * @param int  $length
	 * @return int
	 */
	private static function randAnyIntType(bool $isUnsigned, int $pow, int $length): int
	{
		if ($isUnsigned) {
			$min = fn(): BigInteger => new BigInteger(0);
			$max = function () use ($pow): BigInteger {
				$maxBase = new BigInteger(2);
				$maxExponent = new BigInteger($pow);
				$maxPow = $maxBase->pow($maxExponent);
				return self::roundPhpCanHandle($maxPow);
			};
		} else {
			$min = function () use ($pow): BigInteger {
				$minBase = new BigInteger(-2);
				$minExponent = new BigInteger($pow - 1);
				$minPow = $minBase->pow($minExponent);
				return self::roundPhpCanHandle($minPow);
			};
			$max = function () use ($pow): BigInteger {
				$maxBase = new BigInteger(2);
				$maxExponent = new BigInteger($pow - 1);
				$maxPow = $maxBase->pow($maxExponent)->subtract(new BigInteger(1));
				return self::roundPhpCanHandle($maxPow);
			};
		}

		$result = BigInteger::randomRange($min(), $max())->toString();
		return (int)substr($result, -$length);
	}

	/**
	 * 乱数を返す
	 * ただし PHP で扱える int 型を超える値も扱うことがあるため文字列で返している
	 *
	 * BigInteger が扱うことができる値について
	 * 最小値は pow(-2, 63) + 1=  -9223372036854775807
	 * 最大値は pow(2, 63) - 1=  9223372036854775807
	 *
	 * @param string $min
	 * @param string $max
	 * @return string
	 */
	final public static function randNum(string $min, string $max): string
	{
		$maxBigInteger = new BigInteger($max);
		if ("{$max}" !== $maxBigInteger->toString()) {
			throw new \RangeException('BigInteger で扱えない最大値が指定された');
		}

		$minBigInteger = new BigInteger($min);
		if ("{$min}" !== $minBigInteger->toString()) {
			throw new \RangeException('BigInteger で扱えない最小値が指定された');
		}

		return BigInteger::randomRange($min(), $max())->toString();
	}

	/**
	 * PHP で扱える int 型の最大値を考慮して BigInteger の値を丸める
	 *
	 * @param BigInteger $value
	 * @return BigInteger
	 */
	private static function roundPhpCanHandle(BigInteger $value): BigInteger
	{
		if ($value->isNegative()) {
			// PHP_INT_MIN より小さい数だとオーバーフローして正の数になるため min() で比較する
			$rounded = min((int)$value->toString(), PHP_INT_MIN);
		} else {
			// PHP_INT_MAX より大きい数だとオーバーフローして負の数になるため max() で比較する
			$rounded = max((int)$value->toString(), PHP_INT_MAX);
		}
		return new BigInteger($rounded);
	}
}

Discussion

ログインするとコメントできます