Skip to content

Commit

Permalink
UPDATE:
Browse files Browse the repository at this point in the history
- add the possibility to test any protected or private method in a class
- write new tests for the new feature
  • Loading branch information
rawsrc committed Nov 11, 2021
1 parent 63b8df4 commit 858c65f
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 14 deletions.
82 changes: 77 additions & 5 deletions Pilot.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@
use Closure;
use Exception;

use ReflectionClass;
use ReflectionException;
use ReflectionMethod;

use function count;
use function is_file;

use function is_string;
use function round;

use function str_contains;

use const DIRECTORY_SEPARATOR;

/**
Expand All @@ -43,6 +50,7 @@
*/
class Pilot
{
private static int $counter = -1;
/**
* @var string
*/
Expand Down Expand Up @@ -216,17 +224,65 @@ public function removeResource(string $name): void
* @param Closure $test
* @param string|null $description
* @return int|string Test id
* @throws Exception("Runner's id: {$id} is already defined and locked")
* @throws Exception
*/
public function run(int|string|null $id, Closure $test, string $description = ''): int|string
{
static $i = -1;
$id = $this->getRunnerId($id);
$runner = new Runner($test, $description);
$runner->setId($id);
$this->current_runner = $runner;
$this->runners[$id] = $runner;
$this->milliseconds += $runner->getMilliseconds();

if (isset($id, $this->runners[$id])) {
throw new Exception("Runner's id: {$id} is already defined and locked");
return $id;
}

/**
* For testing purpose of protected or private methods in a class instance
*
* @param int|string|null $id
* @param object|string $class
* @param string $description
* @param string|null $method
* @param array $params
* @return int|string
* @throws Exception("Runner's id: {$id} is already defined and locked")
* @throws ReflectionException
*/
public function runClassMethod(
int|string|null $id,
object|string $class,
string $description = '',
?string $method = null,
array $params = [],
) {
$id = $this->getRunnerId($id);

if (is_string($class)) {
// intercept the short notation class::method
if (str_contains($class, '::')) {
[$class, $method] = explode('::', $class);
}
// the class constructor must not have any required parameters
// otherwise the given class must be already built (object and not a string)
$reflection_class = new ReflectionClass($class);
$constructor = $reflection_class->getConstructor();
if (($constructor !== null) && ($constructor->getNumberOfRequiredParameters()) > 0) {
throw new Exception('The class cannot be a string, it must be an object');
}
$class = new $class;
}
$id ??= ++$i;
$runner = new Runner($test, $description);

if (empty($method)) {
throw new Exception('The method must not be empty');
}

$reflection_method = new ReflectionMethod($class, $method);
$reflection_method->setAccessible(true);

$runner = new Runner(fn() => $reflection_method->invoke($class, ...$params), $description);
$runner->setId($id);
$this->current_runner = $runner;
$this->runners[$id] = $runner;
Expand All @@ -235,6 +291,22 @@ public function run(int|string|null $id, Closure $test, string $description = ''
return $id;
}

/**
* @param int|string|null $id
* @return int|string
* @throws Exception
*/
private function getRunnerId(int|string|null $id): int|string
{
if ($id === null) {
return ++self::$counter;
} elseif (isset($this->runners[$id])) {
throw new Exception("Runner's id: {$id} is already defined and locked");
} else {
return $id;
}
}

/**
* @param int $max_str_length
* @throws Exception
Expand Down
73 changes: 70 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# **Exacodis**

`2021-10-30` `PHP 8.0+` `v.1.1.4`
`2021-11-11` `PHP 8.0+` `v.1.2.0`

## **A PHP TEST ENGINE**

Expand All @@ -23,8 +23,7 @@ override a test run nor a result nor a resource.<br>
If you do, then the code will fail with an `Exception` until you fix the code.

**CHANGELOG**
1. Simplify the extraction of the latest runner by adding a null value to the
default parameter: instead of `$pilot->getRunner(null)`, you have now `$pilot->getRunner()`
1. Add the possibility to test any protected/private method from a class
2. Does not break the compatibility with the previous version

**HOW TO USE**
Expand Down Expand Up @@ -149,6 +148,74 @@ $pilot->assertEqual([
'failed_assertions_percent' => 100-round(17/18*100, 2)
]);
```
- TESTING PROTECTED/PRIVATE METHODS IN CLASSES

To be able to test any protected or private method, you must use `$pilot->runClassMethod(...)`
instead of `$pilot->run(...)`.
The signature of the method is:
```php
public function runClassMethod(
int|string|null $id,
object|string $class,
string $description = '',
?string $method = null,
array $params = [],
)
```
Please note:
- if the class has a complex constructor with required arguments, then you must
provide a clean instance to the var `$class`
- in other cases, `$class` can be a string like `Foo` or even with the method
included: `Foo::method`
- The array `$params` must have all the required parameters for the invocation
of the method. It's also compatible with named parameters.

All the rest is similar to the method `$pilot->run()`.

Let's have an example from the php test file:
Here all tests are equivalent:
```php
$foo = new Foo();
$pilot->runClassMethod(
id: '008',
description: 'private method unit test using directly an instance of Foo',
class: $foo,
method: 'abc',
);
$pilot->assertIsString();
$pilot->assertEqual('abc');

$pilot->runClassMethod(
id: '009',
description: 'private method unit test using string notation for the class Foo',
class: 'Foo',
method: 'abc',
);
$pilot->assertIsString();
$pilot->assertEqual('abc');

$pilot->runClassMethod(
id: '010',
description: 'private method unit test using short string notation for the class Foo and the method abc',
class: 'Foo::abc',
);
$pilot->assertIsString();
$pilot->assertEqual('abc');
```
Have a look at the call of a private method with two parameters
```php
$pilot->runClassMethod(
id: '012',
description: 'private method unit test with two parameters',
class: 'Foo',
method: 'hij',
params: ['p' => 25, 'q' => 50]
);
$pilot->assertIsInt();
$pilot->assertEqual(250);
```
The named parameters must follow the order of the defined parameters.

- REPORT

The engine compute internally the data and, you can ask for a HTML report, as
Expand Down
143 changes: 137 additions & 6 deletions test.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,31 +87,162 @@
$pilot->assertIsArray();
$pilot->assertCount(3);

//region dyanmic assert
//region dynamic assert
$pilot->assert(
test: fn() => count($pilot->getResource('dummy_array_data')) === 3,
test_name: 'Dynamic assertion using manual count',
expected: 3
);
//endregion

//region private/protected methods
class Foo
{
const BAR = 'bar';

private function abc(): string
{
return 'abc';
}

private function def(int $p): int
{
return 2*$p;
}

private function hij(int $p, int $q): int
{
return 2*$p+4*$q;
}

protected function klm(): string
{
return 'klm';
}

protected function nop(int $p): int
{
return 2*$p;
}

protected function qrs(int $p, int $q): int
{
return 2*$p+4*$q;
}
}

$foo = new Foo();
$pilot->runClassMethod(
id: '008',
description: 'private method unit test using directly an instance of Foo',
class: $foo,
method: 'abc',
);
$pilot->assertIsString();
$pilot->assertEqual('abc');

$pilot->runClassMethod(
id: '009',
description: 'private method unit test using string notation for the class Foo',
class: 'Foo',
method: 'abc',
);
$pilot->assertIsString();
$pilot->assertEqual('abc');

$pilot->runClassMethod(
id: '010',
description: 'private method unit test using short string notation for the class Foo and the method abc',
class: 'Foo::abc',
);
$pilot->assertIsString();
$pilot->assertEqual('abc');

$pilot->runClassMethod(
id: '011',
description: 'private method unit test with one parameter',
class: 'Foo',
method: 'def',
params: [25]
);
$pilot->assertIsInt();
$pilot->assertEqual(50);

$pilot->runClassMethod(
id: '012',
description: 'private method unit test with two parameters',
class: 'Foo',
method: 'hij',
params: ['p' => 25, 'q' => 50]
);
$pilot->assertIsInt();
$pilot->assertEqual(250);


$pilot->runClassMethod(
id: '013',
description: 'protected method unit test using directly an instance of Foo',
class: $foo,
method: 'klm',
);
$pilot->assertIsString();
$pilot->assertEqual('klm');

$pilot->runClassMethod(
id: '014',
description: 'protected method unit test using string notation for the class Foo',
class: 'Foo',
method: 'klm',
);
$pilot->assertIsString();
$pilot->assertEqual('klm');

$pilot->runClassMethod(
id: '015',
description: 'protected method unit test using short string notation for the class Foo and the method abc',
class: 'Foo::klm',
);
$pilot->assertIsString();
$pilot->assertEqual('klm');

$pilot->runClassMethod(
id: '016',
description: 'protected method unit test with one parameter',
class: 'Foo',
method: 'nop',
params: [25]
);
$pilot->assertIsInt();
$pilot->assertEqual(50);

$pilot->runClassMethod(
id: '017',
description: 'protected method unit test with two parameters',
class: 'Foo',
method: 'qrs',
params: [25, 50]
);
$pilot->assertIsInt();
$pilot->assertEqual(250);
//endregion

// manual test
$stats = $pilot->getStats();
unset($stats['milliseconds'], $stats['hms']);
$pilot->run(
id: '008',
id: '100',
description: 'check the count',
test: fn() => $stats
);
$pilot->assertIsArray();
$pilot->assertEqual([
'nb_runs' => 7,
'passed_runs' => 7,
'nb_runs' => 17,
'passed_runs' => 17,
'failed_runs' => 0,
'passed_runs_percent' => 100.0,
'failed_runs_percent' => 0.0,
'nb_assertions' => 22,
'passed_assertions' => 22,
'nb_assertions' => 42,
'passed_assertions' => 42,
'failed_assertions' => 0,
'passed_assertions_percent' => 100.0,
'failed_assertions_percent' => 0.0
Expand Down

0 comments on commit 858c65f

Please sign in to comment.