Skip to content
Permalink
Browse files

added Expect for complex assertions formulation (#398)

  • Loading branch information...
dg committed Feb 20, 2019
1 parent f25e58f commit 1ffb7b5b2ee0a6bf53585299eb935215e4bd0ab4
@@ -72,7 +72,7 @@ public static function notSame($expected, $actual, string $description = null):
/**
* Asserts that two values are equal. The identity of objects,
* Asserts that two values are equal and checks expectations. The identity of objects,
* the order of keys in the arrays and marginally different floats are ignored.
*/
public static function equal($expected, $actual, string $description = null): void
@@ -85,13 +85,17 @@ public static function equal($expected, $actual, string $description = null): vo
/**
* Asserts that two values are not equal. The identity of objects,
* Asserts that two values are not equal and checks expectations. The identity of objects,
* the order of keys in the arrays and marginally different floats are ignored.
*/
public static function notEqual($expected, $actual, string $description = null): void
{
self::$counter++;
if (self::isEqual($expected, $actual)) {
try {
$res = self::isEqual($expected, $actual);
} catch (AssertException $e) {
}
if (empty($e) && $res) {
self::fail(self::describe('%1 should not be equal to %2', $description), $actual, $expected);
}
}
@@ -553,7 +557,7 @@ public static function expandMatchingPatterns(string $pattern, $actual): array
/**
* Compares two structures. The identity of objects, the order of keys
* Compares two structures and checks expectations. The identity of objects, the order of keys
* in the arrays and marginally different floats are ignored.
*/
private static function isEqual($expected, $actual, int $level = 0, $objects = null): bool
@@ -562,6 +566,11 @@ private static function isEqual($expected, $actual, int $level = 0, $objects = n
throw new \Exception('Nesting level too deep or recursive dependency.');
}
if ($expected instanceof Expect) {
$expected($actual);
return true;
}
if (is_float($expected) && is_float($actual) && is_finite($expected) && is_finite($actual)) {
$diff = abs($expected - $actual);
return ($diff < self::EPSILON) || ($diff / max(abs($expected), abs($actual)) < self::EPSILON);
@@ -79,6 +79,9 @@ public static function toLine($var): string
} elseif ($var instanceof \Throwable) {
return 'Exception ' . get_class($var) . ': ' . ($var->getCode() ? '#' . $var->getCode() . ' ' : '') . $var->getMessage();
} elseif ($var instanceof Expect) {
return $var->dump();
} elseif (is_object($var)) {
return self::objectToLine($var);
@@ -0,0 +1,122 @@
<?php
/**
* This file is part of the Nette Tester.
* Copyright (c) 2009 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tester;
/**
* Expectations for more complex assertions formulation.
*
* @method static self same($expected)
* @method static self notSame($expected)
* @method static self equal($expected)
* @method static self notEqual($expected)
* @method static self contains($needle)
* @method static self notContains($needle)
* @method static self true()
* @method static self false()
* @method static self null()
* @method static self nan()
* @method static self truthy()
* @method static self falsey()
* @method static self count(int $count)
* @method static self type(string|object $type)
* @method static self match(string $pattern)
* @method static self matchFile(string $file)
*
* @method self andSame($expected)
* @method self andNotSame($expected)
* @method self andEqual($expected)
* @method self andNotEqual($expected)
* @method self andContains($needle)
* @method self andNotContains($needle)
* @method self andTrue()
* @method self andFalse()
* @method self andNull()
* @method self andNan()
* @method self andTruthy()
* @method self andFalsey()
* @method self andCount(int $count)
* @method self andType(string|object $type)
* @method self andMatch(string $pattern)
* @method self andMatchFile(string $file)
*/
class Expect
{
/** @var array of self|\Closure|\stdClass */
private $constraints = [];
public static function __callStatic(string $method, array $args): self
{
$me = new self;
$me->constraints[] = (object) ['method' => $method, 'args' => $args];
return $me;
}
public static function that(callable $constraint): self
{
return (new self)->and($constraint);
}
public function __call(string $method, array $args): self
{
if (preg_match('#^and([A-Z]\w+)#', $method, $m)) {
$this->constraints[] = (object) ['method' => lcfirst($m[1]), 'args' => $args];
return $this;
}
throw new \Error('Call to undefined method ' . __CLASS__ . '::' . $method . '()');
}
public function and(callable $constraint): self
{
$this->constraints[] = $constraint;
return $this;
}
/**
* Checks the expectations.
*/
public function __invoke($actual): void
{
foreach ($this->constraints as $cstr) {
if ($cstr instanceof \stdClass) {
$args = $cstr->args;
$args[] = $actual;
Assert::{$cstr->method}(...$args);
} elseif ($cstr($actual) === false) {
Assert::fail('%1 is expected to be %2', $actual, is_string($cstr) ? $cstr : 'user-expectation');
}
}
}
public function dump(): string
{
$res = [];
foreach ($this->constraints as $cstr) {
if ($cstr instanceof \stdClass) {
$args = isset($cstr->args[0]) ? Dumper::toLine($cstr->args[0]) : '';
$res[] = "$cstr->method($args)";
} elseif ($cstr instanceof self) {
$res[] = $cstr->dump();
} else {
$res[] = is_string($cstr) ? $cstr : 'user-expectation';
}
}
return implode(',', $res);
}
}
@@ -16,6 +16,7 @@
require __DIR__ . '/Framework/TestCase.php';
require __DIR__ . '/Framework/DomQuery.php';
require __DIR__ . '/Framework/FileMutator.php';
require __DIR__ . '/Framework/Expect.php';
require __DIR__ . '/CodeCoverage/Collector.php';
require __DIR__ . '/Runner/Job.php';
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
use Tester\Assert;
use Tester\Expect;
require __DIR__ . '/../bootstrap.php';
Assert::equal(
['a' => Expect::true(), 'b' => Expect::same(10.0)],
['a' => true, 'b' => 10.0]
);
Assert::exception(function () {
Assert::equal(
['a' => Expect::true(), 'b' => Expect::same(10.0)],
['a' => true, 'b' => 10]
);
}, Tester\AssertException::class, '10 should be 10.0');
Assert::equal(
[
'a' => Expect::same(['k1' => 'v1', 'k2' => 'v2']),
'b' => true,
],
[
'b' => true,
'a' => ['k1' => 'v1', 'k2' => 'v2'],
]
);
Assert::exception(function () {
Assert::equal(
[
'a' => Expect::same(['k1' => 'v1', 'k2' => 'v2']),
'b' => true,
],
[
'b' => true,
'a' => ['k2' => 'v2', 'k1' => 'v1'],
]
);
}, Tester\AssertException::class, "['k2' => 'v2', 'k1' => 'v1'] should be ['k1' => 'v1', 'k2' => 'v2']");
@@ -46,13 +46,15 @@ $equals = [
[$obj3, $obj4],
[[0 => 'a', 'str' => 'b'], ['str' => 'b', 0 => 'a']],
[$deep1, $deep2],
[\Tester\Expect::type('int'), 1],
];
$notEquals = [
[1, 1.0],
[INF, -INF],
[['a', 'b'], ['b', 'a']],
[NAN, NAN],
[\Tester\Expect::type('int'), '1', 'string should be int'],
];
@@ -65,12 +67,13 @@ foreach ($equals as [$expected, $value]) {
}, Tester\AssertException::class, '%a% should not be equal to %a%');
}
foreach ($notEquals as [$expected, $value]) {
foreach ($notEquals as $fixture) {
[$expected, $value] = $fixture;
Assert::notEqual($expected, $value);
Assert::exception(function () use ($expected, $value) {
Assert::equal($expected, $value);
}, Tester\AssertException::class, '%a% should be equal to %a%');
}, Tester\AssertException::class, $fixture[2] ?? '%a% should be equal to %a%');
}
Assert::exception(function () {
@@ -21,6 +21,7 @@ $notSame = [
[['a', 'b'], [1 => 'b', 0 => 'a']],
[new stdClass, new stdClass],
[[new stdClass], [new stdClass]],
[\Tester\Expect::type('int'), 1],
];
foreach ($same as [$expected, $value]) {
@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
use Tester\Assert;
use Tester\Expect;
require __DIR__ . '/../bootstrap.php';
// single expectation
$expectation = Expect::type('int');
Assert::same("type('int')", $expectation->dump());
Assert::exception(function () use ($expectation) {
$expectation->__invoke('123');
}, Tester\AssertException::class, 'string should be int');
Assert::noError(function () use ($expectation) {
$expectation->__invoke(123);
});
// expectation + expectation via and()
$expectation = Expect::type('string')->and(Expect::match('%d%'));
Assert::same("type('string'),match('%d%')", $expectation->dump());
Assert::exception(function () use ($expectation) {
$expectation->__invoke(123);
}, Tester\AssertException::class, 'integer should be string');
Assert::noError(function () use ($expectation) {
$expectation->__invoke('123');
});
Assert::exception(function () use ($expectation) {
$expectation->__invoke('abc');
}, Tester\AssertException::class, "'abc' should match '%%d%%'");
// expectation + expectation via andMethod()
$expectation = Expect::type('string')->andMatch('%d%');
Assert::same("type('string'),match('%d%')", $expectation->dump());
Assert::exception(function () use ($expectation) {
$expectation->__invoke(123);
}, Tester\AssertException::class, 'integer should be string');
Assert::noError(function () use ($expectation) {
$expectation->__invoke('123');
});
Assert::exception(function () use ($expectation) {
$expectation->__invoke('abc');
}, Tester\AssertException::class, "'abc' should match '%%d%%'");
// expectation + closure
$expectation = Expect::type('int')->and(function ($val) { return $val > 0; });
Assert::same("type('int'),user-expectation", $expectation->dump());
Assert::exception(function () use ($expectation) {
$expectation->__invoke('123');
}, Tester\AssertException::class, 'string should be int');
Assert::noError(function () use ($expectation) {
$expectation->__invoke(123);
});
Assert::exception(function () use ($expectation) {
$expectation->__invoke(-123);
}, Tester\AssertException::class, "-123 is expected to be 'user-expectation'");
// callable + callable
class Test
{
public function isOdd($val)
{
return (bool) ($val % 2);
}
}
$expectation = Expect::that('is_int')
->and([new Test, 'isOdd']);
Assert::same('is_int,user-expectation', $expectation->dump());
Assert::exception(function () use ($expectation) {
$expectation->__invoke('123');
}, Tester\AssertException::class, "'123' is expected to be 'is_int'");
Assert::noError(function () use ($expectation) {
$expectation->__invoke(123);
});
Assert::exception(function () use ($expectation) {
$expectation->__invoke(124);
}, Tester\AssertException::class, "124 is expected to be 'user-expectation'");

0 comments on commit 1ffb7b5

Please sign in to comment.
You can’t perform that action at this time.