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 zerabool
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
.