Klasy pomocnicze

HttpAssert

Klasa Tester\HttpAssert udostępnia narzędzia do testowania serwerów HTTP. Pozwala łatwo wykonywać żądania HTTP i weryfikować kody statusu, nagłówki oraz treść ciała odpowiedzi za pomocą interfejsu płynnego.

# Podstawowe żądanie HTTP i weryfikacja odpowiedzi
$response = Tester\HttpAssert::fetch('https://example.com/api/users');
$response
	->expectCode(200)
	->expectHeader('Content-Type', contains: 'json')
	->expectBody(contains: 'users');

Metoda fetch() domyślnie tworzy żądanie GET, ale wszystkie parametry da się dostosować:

HttpAssert::fetch(
	'https://api.example.com/users',
	method: 'POST',
	headers: [
		'Authorization' => 'Bearer token123',  # tablica asocjacyjna
		'Accept: application/json',            # albo format tekstowy
	],
	cookies: ['session' => 'abc123'],
	follow: false,                             # nie podążaj za przekierowaniami
	body: '{"name": "John"}'
)
	->expectCode(201);

Kody statusu weryfikuje się metodami expectCode() i denyCode(). Możesz przekazać albo konkretną liczbę, albo funkcję walidującą:

$response
	->expectCode(200)                           # dokładny kod
	->expectCode(fn($code) => $code < 400)      # własna walidacja
	->denyCode(404)                             # nie może być 404
	->denyCode(fn($code) => $code >= 500);      # nie może być błędem serwera

Do weryfikacji nagłówków służą metody expectHeader() i denyHeader(). Możesz sprawdzić, czy nagłówek istnieje, zweryfikować jego dokładną wartość albo dopasować część jego treści:

$response
	->expectHeader('Content-Type')                     # nagłówek musi istnieć
	->expectHeader('Content-Type', 'application/json') # dokładna wartość
	->expectHeader('Content-Type', contains: 'json')   # zawiera tekst
	->expectHeader('Server', matches: 'nginx %a%')     # pasuje do wzorca
	->denyHeader('X-Powered-By')                       # nagłówek nie może istnieć
	->denyHeader('X-Debug', contains: 'sensitive')     # nie może zawierać tekstu
	->denyHeader('X-Debug', matches: '~debug~i');      # nie może pasować do wzorca

Weryfikacja ciała odpowiedzi działa podobnie, metodami expectBody() i denyBody():

$response
	->expectBody('OK')                              # dokładna wartość
	->expectBody(contains: '"status": "success"')   # zawiera fragment JSON
	->expectBody(matches: '%A% hello %A%')          # pasuje do wzorca
	->expectBody(fn($body) => json_decode($body) !== null) # własna walidacja
	->denyBody('Error occurred')                    # nie może mieć dokładnej wartości
	->denyBody(contains: 'error')                   # nie może zawierać tekstu
	->denyBody(matches: '~exception|fatal~i');      # nie może pasować do wzorca

Parametr follow steruje tym, jak HttpAssert obsługuje przekierowania HTTP:

# Testowanie przekierowania bez podążania za nim (domyślne)
HttpAssert::fetch('https://example.com/redirect', follow: false)
	->expectCode(301)
	->expectHeader('Location', 'https://example.com/new-url');

# Podążanie za wszystkimi przekierowaniami do ostatecznej odpowiedzi
HttpAssert::fetch('https://example.com/redirect', follow: true)
	->expectCode(200)
	->expectBody(contains: 'final content');

DomQuery

Tester\DomQuery to klasa rozszerzająca SimpleXMLElement o łatwe wyszukiwanie w HTML albo XML za pomocą selektorów CSS.

# tworzymy DomQuery z ciągu HTML
$dom = Tester\DomQuery::fromHtml('
	<article class="post">
		<h1>Title</h1>
		<div class="content">Text</div>
	</article>
');

# testujemy istnienie elementu selektorami CSS
Assert::true($dom->has('article.post'));
Assert::true($dom->has('h1'));

# znajdujemy elementy jako tablicę obiektów DomQuery
$headings = $dom->find('h1');
Assert::same('Title', (string) $headings[0]);

# testujemy, czy element pasuje do selektora (od wersji 2.5.3)
$content = $dom->find('.content')[0];
Assert::true($content->matches('div'));
Assert::false($content->matches('p'));

# znajdujemy najbliższego przodka pasującego do selektora (od 2.5.5)
$article = $content->closest('.post');
Assert::true($article->matches('article'));

Dla dokumentów XML użyj metody fromXml():

$dom = Tester\DomQuery::fromXml('<catalog><item>First</item></catalog>');
Assert::true($dom->has('item'));

FileMock

Tester\FileMock emuluje pliki w pamięci i ułatwia testowanie kodu używającego funkcji takich jak fopen(), file_get_contents(), parse_ini_file() i podobnych. Przykład użycia:

# Testowana klasa
class Logger
{
	public function __construct(
		private string $logFile,
	) {
	}

	public function log(string $message): void
	{
		file_put_contents($this->logFile, $message . "\n", FILE_APPEND);
	}
}

# Nowy pusty plik
$file = Tester\FileMock::create('');

$logger = new Logger($file);
$logger->log('Login');
$logger->log('Logout');

# Testujemy utworzoną treść
Assert::same("Login\nLogout\n", file_get_contents($file));

Opcjonalny drugi parametr $extension ustawia rozszerzenie pliku w wygenerowanym URL, co przydaje się, gdy testowany kod decyduje na jego podstawie:

$file = Tester\FileMock::create('{"key": "value"}', 'json');

Assert::with()

To nie jest asercja, tylko pomocnik do testowania prywatnych metod i właściwości obiektów.

class Entity
{
	private $enabled;
	// ...
}

$ent = new Entity;

Assert::with($ent, function () {
	Assert::true($this->enabled); // dostępne prywatne $ent->enabled
});

Helpers::purge()

Metoda purge() tworzy podany katalog, a jeśli już istnieje, usuwa całą jego zawartość. Przydaje się do utworzenia katalogu tymczasowego. Na przykład w tests/bootstrap.php:

@mkdir(__DIR__ . '/tmp');  # @ - katalog może już istnieć

define('TempDir', __DIR__ . '/tmp/' . getmypid());
Tester\Helpers::purge(TempDir);

Environment::lock()

Testy działają równolegle. Czasem jednak potrzebujemy, żeby wykonanie testów się nie nakładało. Typowo testy bazodanowe wymagają przygotowania zawartości bazy i zapewnienia, że żaden inny test nie będzie w trakcie ich wykonywania ingerował w bazę. W tych testach używamy Tester\Environment::lock($name, $dir):

Tester\Environment::lock('database', __DIR__ . '/tmp');

Pierwszym parametrem jest nazwa blokady, drugim ścieżka do katalogu przechowywania blokady. Test, który zdobędzie blokadę jako pierwszy, kontynuuje, pozostałe testy muszą czekać na jego zakończenie.

Environment::bypassFinals()

Klasy albo metody oznaczone jako final trudno testować. Wywołanie Tester\Environment::bypassFinals() na początku testu sprawia, że przy wczytywaniu kodu słowa kluczowe final są pomijane.

require __DIR__ . '/bootstrap.php';

Tester\Environment::bypassFinals();

class MyClass extends NormallyFinalClass  # <-- NormallyFinalClass nie jest już final
{
	// ...
}

Environment::setup()

  • poprawia czytelność dumpów błędów (wraz z kolorowaniem); w przeciwnym razie wypisywany jest domyślny stos wywołań PHP
  • włącza sprawdzanie, czy w teście wywołano asercje; w przeciwnym razie testy bez asercji (np. zapomniane) też przechodzą
  • automatycznie uruchamia zbieranie informacji o wykonanym kodzie (gdy używane jest --coverage) (opisane dalej)
  • wypisuje na końcu skryptu status OK albo FAILURE

Environment::setupFunctions()

Tworzy globalne funkcje test(), testException(), testNoError(), setUp() i tearDown(), w które możesz ustrukturyzować swoje testy.

test('test description', function () {
	Assert::same(123, foo());
	Assert::false(bar());
	// ...
});

Environment::VariableRunner

Pozwala ustalić, czy test został uruchomiony bezpośrednio, czy przez Testera.

if (getenv(Tester\Environment::VariableRunner)) {
	# uruchomiony przez Testera
} else {
	# uruchomiony inaczej
}

Environment::VariableThread

Tester uruchamia testy równolegle w podanej liczbie wątków. Jeśli interesuje nas numer wątku, znajdziemy go w zmiennej środowiskowej:

echo "Running in thread number " . getenv(Tester\Environment::VariableThread);