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:
arraylist– tablica indeksowana według rosnącej serii kluczy numerycznych od zeraboolcallablefloatintnullobjectresourcescalarstring- 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.