This commit is contained in:
2026-01-01 21:10:46 +08:00
parent 5c1b19c08a
commit 6c31ffa120
26 changed files with 1171 additions and 209 deletions

340
tests/BaseTestCase.php Normal file
View File

@@ -0,0 +1,340 @@
<?php
/*
* Copyright (c) 2025, Антон Аксенов
* This file is part of m3u.su project
* MIT License: https://git.axenov.dev/IPTV/web/src/branch/master/LICENSE
*/
declare(strict_types=1);
namespace Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
abstract class BaseTestCase extends TestCase
{
/**
* Тестирует наличие методов в классе
*
* @param array $methods
* @param object|string $class
*/
public function assertHasMethods(array $methods, object|string $class): void
{
foreach ($methods as $method) {
$this->assertTrue(
method_exists(is_object($class) ? $class::class : $class, $method),
"Method {$class}::{$method}() does not exist"
);
}
}
protected function makePlaylist(): void
{
}
protected function makeIni(?string $contents = null): string
{
$contents ??= <<<'EOD'
[foo]
name=foo name
desc=foo description
url=http://example.com/foo.m3u
src=http://example.com/
[bar]
name=bar name
desc=bar description
url=http://example.com/bar.m3u
src=http://example.com/
EOD;
return data_stream($contents, 'text/ini');
}
/*
|--------------------------------------------------------------------------
| Методы для заглушки объектов и методов других классов
|--------------------------------------------------------------------------
*/
/**
* Создаёт и возвращает объект HTTP-запроса для тестирования методов контроллеров
*
* @param array $query The GET parameters
* @param array $request The POST parameters
* @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
* @param array $cookies The COOKIE parameters
* @param array $files The FILES parameters
* @param array $server The SERVER parameters
* @param resource|string|null $content The raw body data
*
* @return HttpRequest
*
* @see Request::__construct
*/
protected function makeHttpRequest(
array $query = [],
array $request = [],
array $attributes = [],
array $cookies = [],
array $files = [],
array $server = [],
mixed $content = null
): HttpRequest {
return new HttpRequest(func_get_args());
}
/**
* Возвращает объект ответа HTTP-клиента Laravel
*
* @param int $status
* @param array $headers
* @param $body
* @param string $version
* @param string|null $reason
*
* @return HttpClientResponse
*/
protected function makeHttpResponse(
int $status = 200,
array $headers = [],
$body = null,
string $version = '1.1',
?string $reason = null
): HttpClientResponse {
return new HttpClientResponse(new GuzzleResponse(...func_get_args()));
}
/**
* Вызывает любой метод указанного объекта с нужными аргументами и обходом его видимости
*
* @param class-string|object $objectOrClass
* @param string $method
* @param mixed ...$args
*
* @return mixed
*
* @throws ReflectionException
*
* @see https://stackoverflow.com/questions/249664
* @see \Psy\Sudo::callMethod()
* @see \Psy\Sudo::callStatic()
*/
protected function callMethod(object|string $objectOrClass, string $method, mixed ...$args): mixed
{
$reflObject = is_string($objectOrClass)
? new ReflectionClass($objectOrClass)
: new ReflectionObject($objectOrClass);
$reflMethod = $reflObject->getMethod($method);
return $reflMethod->invokeArgs(is_string($objectOrClass) ? null : $objectOrClass, $args);
}
/*
|--------------------------------------------------------------------------
| Методы-хелперы для подготовки и отладки тестов
|--------------------------------------------------------------------------
*/
/**
* Конвертирует многоуровневый массив в html-файл с таблицей для визуальной
* отладки и сохраняет в `"storage/app/$filename"`.
*
* Использование:
* 0. предполагается во время отладки теста
* 1. вызвать метод, который возвращает массив, приводимый к массиву или читаемый как массив объект
* 2. вызвать `$this->toTable($array, 'test')`
* 3. открыть в браузере файл `storage/app/test.html`
*
* @param array|ArrayAccess $data
* @param string $filename
*/
protected static function toTable(array|ArrayAccess $data, string $filename): void
{
$headers = $result = $html = [];
foreach ($data as $row) {
$result[] = $row = Arr::dot($row);
empty($headers) && $headers = array_keys($row);
}
$html[] = '<html lang="ru"><style>body{margin:0}table{font-family:monospace;border-collapse:collapse}'
. 'thead{background:darkorange;position:sticky;top:-1}th,td{white-space:nowrap;'
. 'border:1px solid black;padding:0 2px}tr:active{font-weight:bold}'
. 'tr:hover{background:lightgrey}</style><body><table><thead>';
foreach ($headers as $header) {
$html[] = "<th>{$header}</th>";
}
$html[] = '</thead><tbody>';
foreach ($result as $row) {
$html[] = '<tr>';
foreach ($row as $value) {
$value instanceof BackedEnum && $value = $value->value;
$value = str_replace("'", '', var_export($value, true)); // строки без кавычек
$html[] = "<td>{$value}</td>";
}
$html[] = '</tr>';
}
$html[] = '</tbody></table></body></html>';
Storage::put("{$filename}.html", implode('', $html));
}
/*
|--------------------------------------------------------------------------
| Методы проверки значений
|--------------------------------------------------------------------------
*/
/**
* Проверяет идентичность двух классов
*
* @param object|string $class1
* @param object|string $class2
*
* @return bool
*/
protected function checkIsSameClass(object|string $class1, object|string $class2): bool
{
return (is_object($class1) ? $class1::class : $class1) === (is_object($class2) ? $class2::class : $class2);
}
/**
* Проверяет наследование других классов указанным
*
* @param string[] $parents Массив имён потенциальных классов-родителей
* @param object|string $class Объект или имя класса для проверки
*
* @see https://www.php.net/manual/en/function.class-parents.php
*/
protected function checkExtendsClasses(array $parents, object|string $class): bool
{
return !empty(array_intersect($parents, is_object($class) ? class_parents($class) : [$class]));
}
/**
* Проверяет реализацию интерфейсов указанным классом
*
* @param string[] $interfaces Массив имён интерфейсов
* @param object|string $class Объект или имя класса для проверки
*
* @see https://www.php.net/manual/en/function.class-parents.php
*/
protected function checkImplementsInterfaces(array $interfaces, object|string $class): bool
{
return !empty(array_intersect($interfaces, is_object($class) ? class_implements($class) : [$class]));
}
/*
|--------------------------------------------------------------------------
| Методы проверки утверждений в тестах
|--------------------------------------------------------------------------
*/
/**
* Утверждает, что в массиве имеются все указанные ключи
*
* @param array $keys Ключи для проверки в массиве
* @param iterable $array Проверяемый массив, итератор или приводимый к массиву объект
*
* @return void
*/
protected function assertArrayHasKeys(array $keys, iterable $array): void
{
$array = iterator_to_array($array);
foreach ($keys as $key) {
$this->assertArrayHasKey($key, $array);
}
}
/**
* Утверждает, что в объекте имеются все указанные свойства
*
* @param array $props Свойства для проверки в объекте
* @param object $object Проверяемый объект
*
* @return void
*/
protected function assertObjectHasProperties(array $props, object $object): void
{
foreach ($props as $prop) {
$this->assertObjectHasProperty($prop, $object);
}
}
/**
* Утверждает, что в массиве отсутствуют все указанные ключи
*
* @param array $keys Ключи для проверки в массиве
* @param iterable $array Проверяемый массив, итератор или приводимый к массиву объект
*
* @return void
*/
protected function assertArrayNotHasKeys(array $keys, iterable $array): void
{
foreach ($keys as $key) {
$this->assertArrayNotHasKey($key, $array);
}
}
/**
* Утверждает, что в объекте отсутствуют все указанные свойства
*
* @param array $props Свойства для проверки в объекте
* @param object $object Проверяемый объект
*
* @return void
*/
protected function assertObjectNotHasProperties(array $props, object $object): void
{
foreach ($props as $prop) {
$this->assertObjectNotHasProperty($prop, $object);
}
}
/**
* Утверждает, что в массиве элементы только указанного типа
*
* @param string $type Название типа, один из возможных результатов функции gettype()
* @param iterable $array Проверяемый массив, итератор или приводимый к массиву объект
*
* @return void
*/
protected function assertArrayValuesTypeOf(string $type, iterable $array): void
{
foreach ($array as $key => $value) {
$this->assertEquals($type, gettype($value), "Failed asserting that element [{$key}] is type of {$type}");
}
}
/**
* Утверждает, что в массив содержит только объекты (опционально -- указанного класса)
*
* Работает гибче {@link self::assertContainsOnlyInstancesOf()}
* засчёт предварительной подготовки проверяемых данных и возможности
* нестрогой проверки имени класса.
*
* @param mixed $array Массив для проверки
* @param object|string|null $class Имя класса (если не указано, проверяется только тип)
*
* @return void
*/
protected function assertIsArrayOfObjects(mixed $array, object|string|null $class = null): void
{
is_object($class) && $class = $class::class;
if (is_string($array) && json_validate($array)) {
$array = json_decode($array);
}
$this->assertNotEmpty($array);
if (empty($class)) {
$filtered = array_filter($array, static fn ($elem) => is_object($elem));
$this->assertSame($array, $filtered, 'Failed asserting that array containts only objects');
} else {
$this->assertContainsOnlyInstancesOf($class, $array);
}
}
}