Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Expect #398

Merged
merged 1 commit into from
Feb 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions src/Framework/Assert.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/Framework/Dumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
122 changes: 122 additions & 0 deletions src/Framework/Expect.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions src/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
48 changes: 48 additions & 0 deletions tests/Framework/Assert.equal.expect.phpt
Original file line number Diff line number Diff line change
@@ -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']");
14 changes: 8 additions & 6 deletions tests/Framework/Assert.equal.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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],
[1, 1.0, null],
[INF, -INF, null],
[['a', 'b'], ['b', 'a'], null],
[NAN, NAN, null],
[\Tester\Expect::type('int'), '1', 'string should be int'],
];


Expand All @@ -65,12 +67,12 @@ foreach ($equals as [$expected, $value]) {
}, Tester\AssertException::class, '%a% should not be equal to %a%');
}

foreach ($notEquals as [$expected, $value]) {
foreach ($notEquals as [$expected, $value, $error]) {
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, $error ?: '%a% should be equal to %a%');
}

Assert::exception(function () {
Expand Down
1 change: 1 addition & 0 deletions tests/Framework/Assert.same.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand Down
103 changes: 103 additions & 0 deletions tests/Framework/Expect.phpt
Original file line number Diff line number Diff line change
@@ -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'");