Asercje
*******
.[perex]
Asercje służą do potwierdzenia, że rzeczywista wartość odpowiada wartości oczekiwanej. Są to metody klasy `Tester\Assert`.
Wybieraj najodpowiedniejsze asercje. `Assert::same($a, $b)` jest lepsze niż `Assert::true($a === $b)`, bo w razie niepowodzenia wyświetla sensowny komunikat o błędzie. W drugim przypadku dostaniemy tylko `false should be true`, co nic nam nie mówi o zawartości zmiennych `$a` i `$b`.
Większość asercji może mieć też opcjonalny opis w parametrze `$description`, który wyświetla się w komunikacie o błędzie, jeśli oczekiwanie nie zostanie spełnione.
Przykłady zakładają utworzony alias:
```php
use Tester\Assert;
```
Assert::same($expected, $actual, ?string $description=null) .[method]
---------------------------------------------------------------------
`$expected` musi być identyczne z `$actual`. To samo co operator PHP `===`.
Assert::notSame($expected, $actual, ?string $description=null) .[method]
------------------------------------------------------------------------
Przeciwieństwo `Assert::same()`, czyli to samo co operator PHP `!==`.
Assert::equal($expected, $actual, ?string $description=null, bool $matchOrder=false, bool $matchIdentity=false) .[method]
-------------------------------------------------------------------------------------------------------------------------
`$expected` musi być równe `$actual`. W przeciwieństwie do `Assert::same()` ignorowana jest tożsamość obiektów, kolejność par klucz => wartość w tablicach i minimalnie różniące się liczby dziesiętne, co można zmienić, ustawiając `$matchIdentity` i `$matchOrder`.
Poniższe przypadki są z perspektywy `equal()` równe, ale z perspektywy `same()` już nie:
```php
Assert::equal(0.3, 0.1 + 0.2);
Assert::equal($obj, clone $obj);
Assert::equal(
['first' => 11, 'second' => 22],
['second' => 22, 'first' => 11],
);
```
Uwaga jednak: tablice `[1, 2]` i `[2, 1]` nie są takie same, bo różni się tylko kolejność wartości, a nie par klucz => wartość. Tablicę `[1, 2]` można zapisać też jako `[0 => 1, 1 => 2]`, a `[1 => 2, 0 => 1]` zostanie więc uznane za taką samą.
W `$expected` możesz też użyć tak zwanych [#Oczekiwania].
Assert::notEqual($expected, $actual, ?string $description=null) .[method]
-------------------------------------------------------------------------
Przeciwieństwo `Assert::equal()`.
Assert::contains($needle, string|array $actual, ?string $description=null) .[method]
------------------------------------------------------------------------------------
Jeśli `$actual` jest ciągiem, musi zawierać podciąg `$needle`. Jeśli jest tablicą, musi zawierać element `$needle` (porównywany ściśle).
Assert::notContains($needle, string|array $actual, ?string $description=null) .[method]
---------------------------------------------------------------------------------------
Przeciwieństwo `Assert::contains()`.
Assert::hasKey(string|int $needle, array $actual, ?string $description=null) .[method]{data-version:2.4.0}
----------------------------------------------------------------------------------------------------------
`$actual` musi być tablicą i musi zawierać klucz `$needle`.
Assert::hasNotKey(string|int $needle, array $actual, ?string $description=null) .[method]{data-version:2.4.0}
-------------------------------------------------------------------------------------------------------------
`$actual` musi być tablicą i nie może zawierać klucza `$needle`.
Assert::true($value, ?string $description=null) .[method]
---------------------------------------------------------
`$value` musi być `true`, czyli `$value === true`.
Assert::truthy($value, ?string $description=null) .[method]
-----------------------------------------------------------
`$value` musi być prawdziwe, czyli spełniać warunek `if ($value) ...`.
Assert::false($value, ?string $description=null) .[method]
----------------------------------------------------------
`$value` musi być `false`, czyli `$value === false`.
Assert::falsey($value, ?string $description=null) .[method]
-----------------------------------------------------------
`$value` musi być fałszywe, czyli spełniać warunek `if (!$value) ...`.
Assert::null($value, ?string $description=null) .[method]
---------------------------------------------------------
`$value` musi być `null`, czyli `$value === null`.
Assert::notNull($value, ?string $description=null) .[method]
------------------------------------------------------------
`$value` nie może być `null`, czyli `$value !== null`.
Assert::nan($value, ?string $description=null) .[method]
--------------------------------------------------------
`$value` musi być Not a Number. Do testowania wartości NAN używaj wyłącznie `Assert::nan()`. Wartość NAN jest bardzo specyficzna, a asercje takie jak `Assert::same()` czy `Assert::equal()` mogą zachowywać się nieoczekiwanie.
Assert::count($count, Countable|array $value, ?string $description=null) .[method]
----------------------------------------------------------------------------------
Liczba elementów w `$value` musi wynosić `$count`. To samo co `count($value) === $count`.
Assert::type(string|object $type, $value, ?string $description=null) .[method]
------------------------------------------------------------------------------
`$value` musi być podanego typu. Jako `$type` możemy użyć ciągu:
- `array`
- `list` - tablica indeksowana według rosnącego szeregu kluczy numerycznych od zera
- `bool`
- `callable`
- `float`
- `int`
- `null`
- `object`
- `resource`
- `scalar`
- `string`
- nazwa klasy albo bezpośrednio obiekt, wtedy musi zachodzić `$value instanceof $type`
Assert::exception(callable $callable, string $class, ?string $message=null, $code=null) .[method]
-------------------------------------------------------------------------------------------------
Przy wywołaniu `$callable` musi zostać rzucony wyjątek klasy `$class`. Jeśli podamy `$message`, komunikat wyjątku musi też [pasować do wzorca |#Assert::match()]. A jeśli podamy `$code`, kody muszą też ściśle się zgadzać.
Na przykład poniższy test nie przejdzie, bo komunikat wyjątku się nie zgadza:
```php
Assert::exception(
fn() => throw new App\InvalidValueException('Zero value'),
App\InvalidValueException::class,
'Value is too low',
);
```
`Assert::exception()` zwraca rzucony wyjątek, co pozwala Ci przetestować także wyjątek zagnieżdżony.
```php
$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) .[method]
----------------------------------------------------------------------------------------
Sprawdza, czy funkcja `$callable` wygenerowała oczekiwane błędy (czyli warningi, notice itd.). Jako `$type` podaj jedną ze stałych `E_...`, na przykład `E_WARNING`. A jeśli podamy `$message`, komunikat o błędzie musi też [pasować do wzorca |#Assert::match()]. Na przykład:
```php
Assert::error(
fn() => $i++,
E_NOTICE,
'Undefined variable: i',
);
```
Jeśli callback generuje więcej błędów, musimy oczekiwać ich wszystkich w dokładnej kolejności. W takim przypadku przekaż w `$type` tablicę:
```php
Assert::error(function () {
$a++;
$b++;
}, [
[E_NOTICE, 'Undefined variable: a'],
[E_NOTICE, 'Undefined variable: b'],
]);
```
.[note]
Jeśli jako `$type` podasz nazwę klasy, zachowa się tak samo jak `Assert::exception()`.
Assert::noError(callable $callable) .[method]
---------------------------------------------
Sprawdza, czy funkcja `$callable` nie wygenerowała żadnego warninga, błędu ani wyjątku. Przydaje się do testowania fragmentów kodu, w których nie ma innej asercji.
Assert::match(string $pattern, $actual, ?string $description=null) .[method]
----------------------------------------------------------------------------
`$actual` musi pasować do wzorca `$pattern`. Możemy użyć dwóch wariantów wzorców: wyrażeń regularnych albo wildcardów.
Jeśli jako `$pattern` przekażemy wyrażenie regularne, musimy użyć do jego ograniczenia `~` albo `#`. Inne ograniczniki nie są wspierane. Na przykład test, w którym `$var` musi zawierać wyłącznie cyfry szesnastkowe:
```php
Assert::match('#^[0-9a-f]+$#i', $var);
```
Drugi wariant jest podobny do porównywania zwykłych ciągów, ale w `$pattern` możemy użyć różnych wildcardów:
- `%a%` jeden albo więcej dowolnych znaków oprócz znaków końca linii
- `%a?%` zero albo więcej dowolnych znaków oprócz znaków końca linii
- `%A%` jeden albo więcej dowolnych znaków wraz ze znakami końca linii
- `%A?%` zero albo więcej dowolnych znaków wraz ze znakami końca linii
- `%s%` jeden albo więcej białych znaków oprócz znaków końca linii
- `%s?%` zero albo więcej białych znaków oprócz znaków końca linii
- `%S%` jeden albo więcej znaków oprócz białych znaków
- `%S?%` zero albo więcej znaków oprócz białych znaków
- `%c%` pojedynczy dowolny znak (oprócz końca linii)
- `%d%` jedna albo więcej cyfr
- `%d?%` zero albo więcej cyfr
- `%i%` wartość całkowita ze znakiem
- `%f%` liczba zmiennoprzecinkowa
- `%h%` jedna albo więcej cyfr szesnastkowych
- `%w%` jeden albo więcej znaków alfanumerycznych
- `%ds%` separator katalogów (`/` albo `\`)
- `%%` jeden znak %
Przykłady:
```php
# Znowu test liczby szesnastkowej
Assert::match('%h%', $var);
# Uogólnienie ścieżki pliku i numeru linii
Assert::match('Error in file %a% on line %i%', $errorMessage);
```
Assert::notMatch(string $pattern, $actual, ?string $description=null) .[method]{data-version:2.5.6}
---------------------------------------------------------------------------------------------------
Przeciwieństwo `Assert::match()`.
Assert::matchFile(string $file, $actual, ?string $description=null) .[method]
-----------------------------------------------------------------------------
Ta asercja jest identyczna z [#Assert::match()], ale wzorzec wczytywany jest z pliku `$file`. Przydaje się to do testowania bardzo długich ciągów. Plik testu pozostaje przejrzysty.
Assert::fail(string $message, $actual=null, $expected=null) .[method]
---------------------------------------------------------------------
Ta asercja zawsze zawodzi. Czasem po prostu się przydaje. Opcjonalnie możemy podać wartość oczekiwaną i rzeczywistą.
Oczekiwania
-----------
Gdy chcemy porównywać bardziej złożone struktury z niestałymi elementami, wspomniane wyżej asercje mogą nie wystarczyć. Testujemy na przykład metodę, która tworzy nowego użytkownika i zwraca jego atrybuty jako tablicę. Nie znamy wartości hasha hasła, ale wiemy, że musi to być ciąg szesnastkowy. A o kolejnym elemencie wiemy tylko, że musi być obiektem `DateTime`.
W takich sytuacjach możemy wewnątrz parametru `$expected` metod `Assert::equal()` i `Assert::notEqual()` użyć `Tester\Expect`, za pomocą którego da się łatwo opisać strukturę.
```php
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 wzorca
'created_at' => Expect::type(DateTime::class), # oczekujemy instancji klasy
], User::create(123, 'milo', 'RandomPaSsWoRd'));
```
Za pomocą `Expect` możemy wykonywać niemal te same asercje co za pomocą `Assert`. Do dyspozycji mamy więc metody `Expect::same()`, `Expect::match()`, `Expect::count()` itd. Poza tym możemy je łączyć w łańcuch:
```php
Expect::type(MyIterator::class)->andCount(5); # oczekujemy MyIterator i liczby elementów 5
```
Alternatywnie możemy napisać własne handlery asercji.
```php
Expect::that(function ($value) {
# zwróć false, jeśli oczekiwanie nie jest spełnione
});
```
Badanie nieudanych asercji
--------------------------
Gdy asercja zawiedzie, Tester wypisuje, na czym polega błąd. Jeśli porównujemy złożone struktury, Tester tworzy dumpy porównywanych wartości i zapisuje je do katalogu `output`. Jeśli na przykład zawiedzie fikcyjny test `Arrays.recursive.phpt`, dumpy zostaną zapisane tak:
```
app/
└── tests/
├── output/
│ ├── Arrays.recursive.actual # wartość rzeczywista
│ └── Arrays.recursive.expected # wartość oczekiwana
│
└── Arrays.recursive.phpt # nieudany test
```
Nazwę katalogu możemy zmienić przez `Tester\Dumper::$dumpDir`.
Asercje
Asercje służą do potwierdzenia, że rzeczywista wartość odpowiada wartości oczekiwanej. Są to metody klasy
Tester\Assert.
Wybieraj najodpowiedniejsze asercje. Assert::same($a, $b) jest lepsze niż Assert::true($a === $b),
bo w razie niepowodzenia wyświetla sensowny komunikat o błędzie. W drugim przypadku dostaniemy tylko
false should be true, co nic nam nie mówi o zawartości zmiennych $a i $b.
Większość asercji może mieć też opcjonalny opis w parametrze $description, który wyświetla się w
komunikacie o błędzie, jeśli oczekiwanie nie zostanie spełnione.
Przykłady zakładają utworzony alias:
use Tester\Assert;
Assert::same ($expected, $actual, ?string
$description=null)
$expected musi być identyczne 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ć równe $actual. W przeciwieństwie do Assert::same() ignorowana jest
tożsamość obiektów, kolejność par klucz ⇒ wartość w tablicach i minimalnie różniące się liczby dziesiętne, co
można zmienić, ustawiając $matchIdentity i $matchOrder.
Poniższe przypadki są z perspektywy equal() równe, ale z perspektywy same() już nie:
Assert::equal(0.3, 0.1 + 0.2);
Assert::equal($obj, clone $obj);
Assert::equal(
['first' => 11, 'second' => 22],
['second' => 22, 'first' => 11],
);
Uwaga jednak: tablice [1, 2] i [2, 1] nie są takie same, bo różni się tylko kolejność
wartości, a nie par klucz ⇒ wartość. Tablicę [1, 2] można zapisać też jako
[0 => 1, 1 => 2], a [1 => 2, 0 => 1] zostanie więc uznane za taką samą.
W $expected możesz też użyć tak zwanych 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, musi zawierać podciąg $needle. Jeśli jest tablicą, musi zawierać
element $needle (porównywany ś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::hasNotKey (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ć prawdziwe, czyli spełniać 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łszywe, czyli spełniać 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, a asercje takie jak Assert::same() czy Assert::equal() mogą
zachowywać się nieoczekiwanie.
Assert::count ($count, Countable|array $value,
?string $description=null)
Liczba elementów w $value musi wynosić $count. To samo co
count($value) === $count.
Assert::type (string|object $type, $value,
?string $description=null)
$value musi być podanego typu. Jako $type możemy użyć ciągu:
array
list – tablica indeksowana według rosnącego szeregu kluczy numerycznych od zera
bool
callable
float
int
null
object
resource
scalar
string
- nazwa klasy albo bezpośrednio obiekt, wtedy musi zachodzić
$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, komunikat wyjątku musi też pasować do wzorca. A jeśli podamy
$code, kody muszą też ściśle się zgadzać.
Na przykład poniższy test nie przejdzie, bo komunikat wyjątku się nie zgadza:
Assert::exception(
fn() => throw new App\InvalidValueException('Zero value'),
App\InvalidValueException::class,
'Value is too low',
);
Assert::exception() zwraca rzucony wyjątek, co pozwala Ci przetestować także 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 (czyli warningi, notice itd.). Jako
$type podaj jedną ze stałych E_..., na przykład E_WARNING. A jeśli podamy
$message, komunikat o błędzie musi też pasować do wzorca. Na przykład:
Assert::error(
fn() => $i++,
E_NOTICE,
'Undefined variable: i',
);
Jeśli callback generuje więcej błędów, musimy oczekiwać ich wszystkich w dokładnej kolejności. W takim przypadku
przekaż 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, zachowa się tak samo jak
Assert::exception().
Assert::noError (callable
$callable)
Sprawdza, czy funkcja $callable nie wygenerowała żadnego warninga, błędu ani wyjątku. Przydaje się do
testowania fragmentów kodu, w których nie ma innej asercji.
Assert::match (string $pattern, $actual,
?string $description=null)
$actual musi pasować do wzorca $pattern. Możemy użyć dwóch wariantów wzorców: wyrażeń
regularnych albo wildcardów.
Jeśli jako $pattern przekażemy wyrażenie regularne, musimy użyć do jego ograniczenia ~ albo
#. Inne ograniczniki nie są wspierane. Na przykład test, w którym $var musi zawierać wyłącznie
cyfry szesnastkowe:
Assert::match('#^[0-9a-f]+$#i', $var);
Drugi wariant jest podobny do porównywania zwykłych ciągów, ale w $pattern możemy użyć różnych
wildcardów:
%a% jeden albo więcej dowolnych znaków oprócz znaków końca linii
%a?% zero albo więcej dowolnych znaków oprócz znaków końca linii
%A% jeden albo więcej dowolnych znaków wraz ze znakami końca linii
%A?% zero albo więcej dowolnych znaków wraz ze znakami końca linii
%s% jeden albo więcej białych znaków oprócz znaków końca linii
%s?% zero albo więcej białych znaków oprócz znaków końca linii
%S% jeden albo więcej znaków oprócz białych znaków
%S?% zero albo więcej znaków oprócz białych znaków
%c% pojedynczy dowolny znak (oprócz końca linii)
%d% jedna albo więcej cyfr
%d?% zero albo więcej cyfr
%i% wartość całkowita ze znakiem
%f% liczba zmiennoprzecinkowa
%h% jedna albo więcej cyfr szesnastkowych
%w% jeden albo więcej znaków alfanumerycznych
%ds% separator katalogów (/ albo \)
%% jeden znak %
Przykłady:
# Znowu test liczby szesnastkowej
Assert::match('%h%', $var);
# Uogólnienie ścieżki pliku i numeru linii
Assert::match('Error in file %a% on line %i%', $errorMessage);
Assert::notMatch (string
$pattern, $actual, ?string $description=null)
Przeciwieństwo Assert::match().
Assert::matchFile (string $file, $actual,
?string $description=null)
Ta asercja jest identyczna z Assert::match(), ale wzorzec wczytywany jest z pliku
$file. Przydaje się to do testowania bardzo długich ciągów. Plik testu pozostaje przejrzysty.
Assert::fail (string $message, $actual=null,
$expected=null)
Ta asercja zawsze zawodzi. Czasem po prostu się przydaje. Opcjonalnie możemy podać wartość oczekiwaną
i rzeczywistą.
Oczekiwania
Gdy chcemy porównywać bardziej złożone struktury z niestałymi elementami, wspomniane wyżej asercje mogą nie
wystarczyć. Testujemy na przykład metodę, która tworzy nowego użytkownika i zwraca jego atrybuty jako tablicę. Nie znamy
wartości hasha hasła, ale wiemy, że musi to być ciąg szesnastkowy. A o kolejnym elemencie wiemy tylko, że musi być
obiektem DateTime.
W takich sytuacjach możemy wewnątrz parametru $expected metod Assert::equal() i
Assert::notEqual() użyć Tester\Expect, za pomocą którego da się ł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 wzorca
'created_at' => Expect::type(DateTime::class), # oczekujemy instancji klasy
], User::create(123, 'milo', 'RandomPaSsWoRd'));
Za pomocą Expect możemy wykonywać niemal te same asercje co za pomocą Assert. Do dyspozycji mamy
więc metody Expect::same(), Expect::match(), Expect::count() itd. Poza tym możemy je
łączyć w łańcuch:
Expect::type(MyIterator::class)->andCount(5); # oczekujemy MyIterator i liczby elementów 5
Alternatywnie możemy napisać własne handlery asercji.
Expect::that(function ($value) {
# zwróć false, jeśli oczekiwanie nie jest spełnione
});
Badanie nieudanych asercji
Gdy asercja zawiedzie, Tester wypisuje, na czym polega błąd. Jeśli porównujemy złożone struktury, Tester tworzy dumpy
porównywanych wartości i zapisuje je do katalogu output. Jeśli na przykład zawiedzie fikcyjny test
Arrays.recursive.phpt, dumpy zostaną zapisane tak:
app/
└── tests/
├── output/
│ ├── Arrays.recursive.actual # wartość rzeczywista
│ └── Arrays.recursive.expected # wartość oczekiwana
│
└── Arrays.recursive.phpt # nieudany test
Nazwę katalogu możemy zmienić przez Tester\Dumper::$dumpDir.