Asercje

Asercje służą do potwierdzenia, że rzeczywista wartość odpowiada oczekiwanej wartości. Są to metody klasy Tester\Assert.

Wybieraj jak najbardziej odpowiednie asercje. Lepsze jest Assert::same($a, $b) niż Assert::true($a === $b), ponieważ przy niepowodzeniu wyświetli sensowny komunikat błędu. W drugim przypadku tylko false should be true, co nic nam nie mówi o zawartości zmiennych $a i $b.

Większość asercji może również mieć opcjonalny opis w parametrze $description, który zostanie wyświetlony w komunikacie błędu, jeśli oczekiwanie zawiedzie.

Przykłady zakładają utworzony alias:

use Tester\Assert;

Assert::same ($expected, $actual, ?string $description=null)

$expected musi być identyczny z $actual. To samo co operator PHP ===.

Assert::notSame ($expected, $actual, ?string $description=null)

Przeciwieństwo Assert::same(), czyli to samo co operator PHP !==.

Assert::equal ($expected, $actual, ?string $description=null, bool $matchOrder=false, bool $matchIdentity=false)

$expected musi być taki sam jak $actual. W przeciwieństwie do Assert::same() ignoruje się tożsamość obiektów, kolejność par klucz ⇒ wartość w tablicach i marginalnie różne liczby dziesiętne, co można zmienić ustawieniem $matchIdentity i $matchOrder.

Następujące przypadki są zgodne z punktu widzenia equal(), ale nie same():

Assert::equal(0.3, 0.1 + 0.2);
Assert::equal($obj, clone $obj);
Assert::equal(
	['first' => 11, 'second' => 22],
	['second' => 22, 'first' => 11],
);

Jednak uwaga, tablice [1, 2] i [2, 1] nie są takie same, ponieważ różnią się tylko kolejnością wartości, a nie par klucz ⇒ wartość. Tablicę [1, 2] można zapisać również jako [0 => 1, 1 => 2] i za taką samą będzie uważana [1 => 2, 0 => 1].

Dalej w $expected można użyć tzw. Oczekiwania.

Assert::notEqual ($expected, $actual, ?string $description=null)

Przeciwieństwo Assert::equal().

Assert::contains ($needle, string|array $actual, ?string $description=null)

Jeśli $actual jest ciągiem znaków, musi zawierać podciąg $needle. Jeśli jest tablicą, musi zawierać element $needle (porównuje się ściśle).

Assert::notContains ($needle, string|array $actual, ?string $description=null)

Przeciwieństwo Assert::contains().

Assert::hasKey (string|int $needle, array $actual, ?string $description=null)

$actual musi być tablicą i musi zawierać klucz $needle.

Assert::notHasKey (string|int $needle, array $actual, ?string $description=null)

$actual musi być tablicą i nie może zawierać klucza $needle.

Assert::true ($value, ?string $description=null)

$value musi być true, czyli $value === true.

Assert::truthy ($value, ?string $description=null)

$value musi być prawdziwy, czyli spełni warunek if ($value) ....

Assert::false ($value, ?string $description=null)

$value musi być false, czyli $value === false.

Assert::falsey ($value, ?string $description=null)

$value musi być fałszywy, czyli spełni warunek if (!$value) ....

Assert::null ($value, ?string $description=null)

$value musi być null, czyli $value === null.

Assert::notNull ($value, ?string $description=null)

$value nie może być null, czyli $value !== null.

Assert::nan ($value, ?string $description=null)

$value musi być Not a Number. Do testowania wartości NAN używaj wyłącznie Assert::nan(). Wartość NAN jest bardzo specyficzna i asercje Assert::same() lub Assert::equal() mogą działać nieoczekiwanie.

Assert::count ($count, Countable|array $value, ?string $description=null)

Liczba elementów w $value musi być $count. Czyli to samo co count($value) === $count.

Assert::type (string|object $type, $value, ?string $description=null)

$value musi być danego typu. Jako $type możemy użyć ciągu znaków:

  • array
  • list – tablica indeksowana według rosnącej serii kluczy numerycznych od zera
  • bool
  • callable
  • float
  • int
  • null
  • object
  • resource
  • scalar
  • string
  • nazwa klasy lub bezpośrednio obiekt, wtedy musi być $value instanceof $type

Assert::exception (callable $callable, string $class, ?string $message=null, $code=null)

Przy wywołaniu $callable musi zostać rzucony wyjątek klasy $class. Jeśli podamy $message, musi odpowiadać wzorowi również komunikat wyjątku, a jeśli podamy $code, muszą się ściśle zgadzać również kody.

Następujący test zawiedzie, ponieważ nie odpowiada komunikat wyjątku:

Assert::exception(
	fn() => throw new App\InvalidValueException('Zero value'),
	App\InvalidValueException::class,
	'Value is to low',
);

Assert::exception() zwraca rzucony wyjątek, można więc przetestować również wyjątek zagnieżdżony.

$e = Assert::exception(
	fn() => throw new MyException('Something is wrong', 0, new RuntimeException),
	MyException::class,
	'Something is wrong',
);

Assert::type(RuntimeException::class, $e->getPrevious());

Assert::error (string $callable, int|string|array $type, ?string $message=null)

Sprawdza, czy funkcja $callable wygenerowała oczekiwane błędy (tj. ostrzeżenia, notices itp.). Jako $type podamy jedną ze stałych E_..., czyli na przykład E_WARNING. A jeśli podamy $message, musi odpowiadać wzorowi również komunikat błędu. Na przykład:

Assert::error(
	fn() => $i++,
	E_NOTICE,
	'Undefined variable: i',
);

Jeśli callback wygeneruje więcej błędów, musimy je wszystkie oczekiwać w dokładnej kolejności. W takim przypadku przekażemy w $type tablicę:

Assert::error(function () {
	$a++;
	$b++;
}, [
	[E_NOTICE, 'Undefined variable: a'],
	[E_NOTICE, 'Undefined variable: b'],
]);

Jeśli jako $type podasz nazwę klasy, zachowuje się tak samo jak Assert::exception().

Assert::noError (callable $callable)

Sprawdza, czy funkcja $callable nie wygenerowała żadnego ostrzeżenia, błędu ani wyjątku. Przydaje się do testowania fragmentów kodu, gdzie nie ma żadnej innej asercji.

Assert::match (string $pattern, $actual, ?string $description=null)

$actual musi pasować do wzoru $pattern. Możemy użyć dwóch wariantów wzorów: wyrażeń regularnych lub symboli wieloznacznych.

Jeśli jako $pattern przekażemy wyrażenie regularne, do jego ograniczenia musimy użyć ~ lub #, inne ograniczniki nie są obsługiwane. Na przykład test, gdy $var musi zawierać tylko cyfry heksadecymalne:

Assert::match('#^[0-9a-f]$#i', $var);

Druga warianta jest podobna do zwykłego porównania ciągów znaków, ale w $pattern możemy użyć różnych symboli wieloznacznych:

  • %a% jeden lub więcej znaków, oprócz znaków końca linii
  • %a?% zero lub więcej znaków, oprócz znaków końca linii
  • %A% jeden lub więcej znaków, włącznie ze znakami końca linii
  • %A?% zero lub więcej znaków, włącznie ze znakami końca linii
  • %s% jeden lub więcej białych znaków, oprócz znaków końca linii
  • %s?% zero lub więcej białych znaków, oprócz znaków końca linii
  • %S% jeden lub więcej znaków, oprócz białych znaków
  • %S?% zero lub więcej znaków, oprócz białych znaków
  • %c% jakikolwiek jeden znak, oprócz znaku końca linii
  • %d% jedna lub więcej cyfr
  • %d?% zero lub więcej cyfr
  • %i% wartość całkowita ze znakiem
  • %f% liczba z przecinkiem dziesiętnym
  • %h% jedna lub więcej cyfr heksadecymalnych
  • %w% jeden lub więcej znaków alfanumerycznych
  • %% znak %

Przykłady:

# Ponownie test na liczbę heksadecymalną
Assert::match('%h%', $var);

# Uogólnienie ścieżki do pliku i numeru linii
Assert::match('Error in file %a% on line %i%', $errorMessage);

Assert::matchFile (string $file, $actual, ?string $description=null)

Asercja jest identyczna z Assert::match(), ale wzór jest wczytywany z pliku $file. Jest to przydatne do testowania bardzo długich ciągów znaków. Plik z testem pozostanie przejrzysty.

Assert::fail (string $message, $actual=null, $expected=null)

Ta asercja zawsze zawiedzie. Czasami po prostu się przydaje. Opcjonalnie możemy podać również oczekiwaną i aktualną wartość.

Oczekiwania

Gdy chcemy porównać bardziej złożone struktury z niestałymi elementami, powyższe asercje mogą nie być wystarczające. Na przykład testujemy metodę, która tworzy nowego użytkownika i zwraca jego atrybuty jako tablicę. Wartości hasha hasła nie znamy, ale wiemy, że musi to być ciąg heksadecymalny. A o kolejnym elemencie wiemy tylko, że musi to być obiekt DateTime.

W tych sytuacjach możemy użyć Tester\Expect wewnątrz parametru $expected metod Assert::equal() i Assert::notEqual(), za pomocą których można łatwo opisać strukturę.

use Tester\Expect;

Assert::equal([
	'id' => Expect::type('int'),                   # oczekujemy liczby całkowitej
	'username' => 'milo',
	'password' => Expect::match('%h%'),            # oczekujemy ciągu pasującego do wzoru
	'created_at' => Expect::type(DateTime::class), # oczekujemy instancji klasy
], User::create(123, 'milo', 'RandomPaSsWoRd'));

Z Expect możemy wykonywać prawie takie same asercje jak z Assert. Czyli mamy do dyspozycji metody Expect::same(), Expect::match(), Expect::count() itd. Ponadto możemy je łączyć w łańcuchy:

Expect::type(MyIterator::class)->andCount(5);  # oczekujemy MyIterator i liczby elementów 5

Albo możemy pisać własne handlery asercji.

Expect::that(function ($value) {
	# zwrócimy false, jeśli oczekiwanie zawiedzie
});

Badanie błędnych asercji

Gdy asercja zawiedzie, Tester wypisze, na czym polega błąd. Jeśli porównujemy bardziej złożone struktury, Tester utworzy zrzuty porównywanych wartości i zapisze je w katalogu output. Na przykład przy niepowodzeniu fikcyjnego testu Arrays.recursive.phpt zrzuty zostaną zapisane następująco:

app/
└── tests/
	├── output/
	│   ├── Arrays.recursive.actual    # aktualna wartość
	│   └── Arrays.recursive.expected  # oczekiwana wartość
	│
	└── Arrays.recursive.phpt          # zawodzący test

Nazwę katalogu możemy zmienić przez Tester\Dumper::$dumpDir.