Aserțiuni

Aserțiunile sunt folosite pentru a confirma că valoarea reală corespunde valorii așteptate. Acestea sunt metode ale clasei Tester\Assert.

Alegeți aserțiunile cele mai potrivite. Este mai bine Assert::same($a, $b) decât Assert::true($a === $b), deoarece în caz de eșec afișează un mesaj de eroare semnificativ. În al doilea caz, doar false should be true, ceea ce nu ne spune nimic despre conținutul variabilelor $a și $b.

Majoritatea aserțiunilor pot avea și o descriere opțională în parametrul $description, care se va afișa în mesajul de eroare dacă așteptarea eșuează.

Exemplele presupun crearea unui alias:

use Tester\Assert;

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

$expected trebuie să fie identic cu $actual. Același lucru ca operatorul PHP ===.

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

Opusul Assert::same(), adică același lucru ca operatorul PHP !==.

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

$expected trebuie să fie egal cu $actual. Spre deosebire de Assert::same(), se ignoră identitatea obiectelor, ordinea perechilor cheie ⇒ valoare în array-uri și numerele zecimale marginal diferite, ceea ce poate fi schimbat prin setarea $matchIdentity și $matchOrder.

Următoarele cazuri sunt egale din punctul de vedere al equal(), dar nu și same():

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

Atenție însă, array-urile [1, 2] și [2, 1] nu sunt egale, deoarece diferă doar ordinea valorilor, nu și a perechilor cheie ⇒ valoare. Array-ul [1, 2] poate fi scris și ca [0 => 1, 1 => 2] și, prin urmare, [1 => 2, 0 => 1] va fi considerat egal.

Mai departe, în $expected se pot folosi așa-numitele așteptări.

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

Opusul Assert::equal().

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

Dacă $actual este un șir, trebuie să conțină subșirul $needle. Dacă este un array, trebuie să conțină elementul $needle (se compară strict).

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

Opusul Assert::contains().

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

$actual trebuie să fie un array și trebuie să conțină cheia $needle.

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

$actual trebuie să fie un array și nu trebuie să conțină cheia $needle.

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

$value trebuie să fie true, adică $value === true.

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

$value trebuie să fie adevărat (truthy), adică îndeplinește condiția if ($value) ....

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

$value trebuie să fie false, adică $value === false.

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

$value trebuie să fie fals (falsey), adică îndeplinește condiția if (!$value) ....

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

$value trebuie să fie null, adică $value === null.

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

$value nu trebuie să fie null, adică $value !== null.

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

$value trebuie să fie Not a Number. Pentru testarea valorii NAN folosiți exclusiv Assert::nan(). Valoarea NAN este foarte specifică și aserțiunile Assert::same() sau Assert::equal() pot funcționa neașteptat.

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

Numărul de elemente din $value trebuie să fie $count. Adică același lucru ca count($value) === $count.

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

$value trebuie să fie de tipul dat. Ca $type putem folosi un șir:

  • array
  • list – array indexat după o serie ascendentă de chei numerice începând de la zero
  • bool
  • callable
  • float
  • int
  • null
  • object
  • resource
  • scalar
  • string
  • numele clasei sau direct obiectul, atunci $value trebuie să fie instanceof $type

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

La apelarea $callable trebuie să fie aruncată o excepție de clasa $class. Dacă specificăm $message, trebuie să corespundă modelului și mesajul excepției, iar dacă specificăm $code, trebuie să se potrivească strict și codurile.

Următorul test va eșua, deoarece mesajul excepției nu corespunde:

Assert::exception(
	fn() => throw new App\InvalidValueException('Valoare zero'),
	App\InvalidValueException::class,
	'Valoarea este prea mică',
);

Assert::exception() returnează excepția aruncată, astfel se poate testa și o excepție imbricată.

$e = Assert::exception(
	fn() => throw new MyException('Ceva este în neregulă', 0, new RuntimeException),
	MyException::class,
	'Ceva este în neregulă',
);

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

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

Verifică dacă funcția $callable a generat erorile așteptate (adică avertismente, notificări etc.). Ca $type specificăm una dintre constantele E_..., de exemplu E_WARNING. Și dacă specificăm $message, trebuie să corespundă modelului și mesajul de eroare. De exemplu:

Assert::error(
	fn() => $i++,
	E_NOTICE,
	'Variabilă nedefinită: i',
);

Dacă callback-ul generează mai multe erori, trebuie să le așteptăm pe toate în ordinea exactă. În acest caz, transmitem în $type un array:

Assert::error(function () {
	$a++;
	$b++;
}, [
	[E_NOTICE, 'Variabilă nedefinită: a'],
	[E_NOTICE, 'Variabilă nedefinită: b'],
]);

Dacă specificați ca $type numele unei clase, se comportă la fel ca Assert::exception().

Assert::noError (callable $callable)

Verifică dacă funcția $callable nu a generat niciun avertisment, eroare sau excepție. Este util pentru testarea bucăților de cod unde nu există nicio altă aserțiune.

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

$actual trebuie să corespundă modelului $pattern. Putem folosi două variante de modele: expresii regulate sau substituenți.

Dacă transmitem ca $pattern o expresie regulată, pentru delimitarea sa trebuie să folosim ~ sau #, alți delimitatori nu sunt suportați. De exemplu, testul în care $var trebuie să conțină doar cifre hexazecimale:

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

A doua variantă este similară cu compararea obișnuită a șirurilor, dar în $pattern putem folosi diferiți substituenți:

  • %a% unul sau mai multe caractere, cu excepția caracterelor de sfârșit de linie
  • %a?% zero sau mai multe caractere, cu excepția caracterelor de sfârșit de linie
  • %A% unul sau mai multe caractere, inclusiv caracterele de sfârșit de linie
  • %A?% zero sau mai multe caractere, inclusiv caracterele de sfârșit de linie
  • %s% unul sau mai multe caractere spațiu alb, cu excepția caracterelor de sfârșit de linie
  • %s?% zero sau mai multe caractere spațiu alb, cu excepția caracterelor de sfârșit de linie
  • %S% unul sau mai multe caractere, cu excepția caracterelor spațiu alb
  • %S?% zero sau mai multe caractere, cu excepția caracterelor spațiu alb
  • %c% orice caracter unic, cu excepția caracterului de sfârșit de linie
  • %d% una sau mai multe cifre
  • %d?% zero sau mai multe cifre
  • %i% valoare întreagă cu semn
  • %f% număr zecimal
  • %h% una sau mai multe cifre hexazecimale
  • %w% unul sau mai multe caractere alfanumerice
  • %% caracterul %

Exemple:

# Din nou test pentru număr hexazecimal
Assert::match('%h%', $var);

# Generalizarea căii către fișier și a numărului liniei
Assert::match('Eroare în fișierul %a% la linia %i%', $errorMessage);

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

Aserțiunea este identică cu Assert::match(), dar modelul se încarcă din fișierul $file. Acest lucru este util pentru testarea șirurilor foarte lungi. Fișierul cu testul rămâne lizibil.

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

Această aserțiune eșuează întotdeauna. Uneori este pur și simplu util. Opțional, putem specifica și valoarea așteptată și cea reală.

Așteptări

Când dorim să comparăm structuri mai complexe cu elemente neconstante, aserțiunile de mai sus s-ar putea să nu fie suficiente. De exemplu, testăm o metodă care creează un nou utilizator și returnează atributele sale ca array. Nu cunoaștem valoarea hash-ului parolei, dar știm că trebuie să fie un șir hexazecimal. Și despre un alt element știm doar că trebuie să fie un obiect DateTime.

În aceste situații putem folosi Tester\Expect în interiorul parametrului $expected al metodelor Assert::equal() și Assert::notEqual(), cu ajutorul cărora structura poate fi descrisă ușor.

use Tester\Expect;

Assert::equal([
	'id' => Expect::type('int'),                   # așteptăm un număr întreg
	'username' => 'milo',
	'password' => Expect::match('%h%'),            # așteptăm un șir care corespunde modelului
	'created_at' => Expect::type(DateTime::class), # așteptăm o instanță a clasei
], User::create(123, 'milo', 'RandomPaSsWoRd'));

Cu Expect putem efectua aproape aceleași aserțiuni ca și cu Assert. Adică avem la dispoziție metodele Expect::same(), Expect::match(), Expect::count() etc. În plus, le putem înlănțui:

Expect::type(MyIterator::class)->andCount(5);  # așteptăm MyIterator și numărul de elemente 5

Sau putem scrie proprii handleri de aserțiuni.

Expect::that(function ($value) {
	# returnăm false dacă așteptarea eșuează
});

Examinarea aserțiunilor eșuate

Când o aserțiune eșuează, Tester afișează unde este greșeala. Dacă comparăm structuri mai complexe, Tester creează dump-uri ale valorilor comparate și le salvează în directorul output. De exemplu, la eșecul testului fictiv Arrays.recursive.phpt, dump-urile vor fi salvate astfel:

app/
└── tests/
	├── output/
	│   ├── Arrays.recursive.actual    # valoarea actuală
	│   └── Arrays.recursive.expected  # valoarea așteptată
	│
	└── Arrays.recursive.phpt          # testul eșuat

Numele directorului poate fi schimbat prin Tester\Dumper::$dumpDir.