diff --git a/Pilot.php b/Pilot.php index a9fcf36..3d1c3c5 100644 --- a/Pilot.php +++ b/Pilot.php @@ -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; /** @@ -43,6 +50,7 @@ */ class Pilot { + private static int $counter = -1; /** * @var string */ @@ -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; @@ -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 diff --git a/README.md b/README.md index 741bbbd..e1e6044 100644 --- a/README.md +++ b/README.md @@ -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** @@ -23,8 +23,7 @@ override a test run nor a result nor a resource.
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** @@ -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 diff --git a/test.php b/test.php index bd0dc5e..013ebe9 100644 --- a/test.php +++ b/test.php @@ -87,7 +87,7 @@ $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', @@ -95,23 +95,154 @@ ); //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