From 39959e1d3324e28079bb1acd7ae3963eadf9280c Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Mon, 23 Nov 2020 20:48:31 -0500 Subject: [PATCH] Define a Methods trait This trait exposes three methods: 1. `defineMethod(string $class, string $name, \Closure $closure, string $visibility = 'public', bool $static = false): self` 2. `redefineMethod(string $class, string $name, ?\Closure $closure, ?string $visibility = null, ?bool $static = null): self` 3. `deleteMethod(string $class, string $name): self` Refs #12. --- README.md | 1 + docs/Methods.md | 100 ++++++ src/Exceptions/MethodExistsException.php | 8 + src/Methods.php | 213 ++++++++++++ src/Support/Runkit.php | 75 ++++- tests/MethodsTest.php | 399 +++++++++++++++++++++++ tests/Support/RunkitTest.php | 18 + tests/TestCase.php | 2 + tests/stubs/TestClass.php | 66 ++++ 9 files changed, 881 insertions(+), 1 deletion(-) create mode 100644 docs/Methods.md create mode 100644 src/Exceptions/MethodExistsException.php create mode 100644 src/Methods.php create mode 100644 tests/MethodsTest.php create mode 100644 tests/stubs/TestClass.php diff --git a/README.md b/README.md index 31f70c7..374b8e9 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ The library offers a number of traits, based on the type of global state that mi * [Environment Variables](docs/EnvironmentVariables.md) * [Functions](docs/Functions.md) (requires [Runkit7]) * [Global Variables](docs/GlobalVariables.md) +* [Methods](docs/Methods.md) (requires[Runkit7]) ## Contributing diff --git a/docs/Methods.md b/docs/Methods.md new file mode 100644 index 0000000..7d38431 --- /dev/null +++ b/docs/Methods.md @@ -0,0 +1,100 @@ +# Managing Methods + +When writing tests, we often make use of [test doubles](https://en.wikipedia.org/wiki/Test_double) to better control how our code will behave. For instance, we don't want to make calls to remote APIs every time we run our tests, as these dependencies can make our tests fragile and slow. + +If your software is written using proper [Dependency Injection](https://phptherightway.com/#dependency_injection), it's usually pretty easy to [create test doubles with PHPUnit](https://jmauerhan.wordpress.com/2018/10/04/the-5-types-of-test-doubles-and-how-to-create-them-in-phpunit/) and inject them into the objects we create in our tests. + +What happens when the software we're working with isn't coded so nicely, though? + +Most of the time, we can get around this using [Reflection](https://www.php.net/intro.reflection), but _sometimes_ we need a sledgehammer to break through. That's where the `AssertWell\PHPUnitGlobalState\Methods` trait (powered by [PHP's runkit7 extension](Runkit.md)) comes in handy. + + +## Methods + +As all of these methods require [runkit7](Runkit.md), tests that use these methods will automatically be marked as skipped if the extension is unavailable. + +--- + +### defineMethod() + +Define a new method for the duration of the test. + +`defineMethod(string $class, string $name, \Closure $closure, string $visibility = 'public', bool $static = false): self` + +This is a wrapper around [PHP's `runkit7_method_define()` function](https://www.php.net/manual/en/function.runkit7-method-define.php). + +#### Parameters + +
+
$class
+
The class name.
+
$name
+
The method name.
+
$closure
+
The code for the method.
+
$visibility
+
Optional. The method visibility, one of "public", "protected", or "private".
+
$static
+
Optional. Whether or not the method should be static. Default is false.
+
+ +#### Return values + +This method will return the calling class, enabling multiple methods to be chained. + +An `AssertWell\PHPUnitGlobalState\Exceptions\MethodExistsException` exception will be thrown if the given `$method` already exists. An `AssertWell\PHPUnitGlobalState\Exceptions\RunkitException` will be thrown if the given method cannot be defined. + +--- + +### redefineMethod() + +Redefine an existing method for the duration of the test. If `$name` does not exist, it will be defined. + +`redefineMethod(string $class, string $name, ?\Closure $closure, ?string $visibility = null, ?bool $static = null): self` + +This is a wrapper around [PHP's `runkit7_method_redefine()` function](https://www.php.net/manual/en/function.runkit7-method-redefine.php). + +#### Parameters + +
+
$class
+
The class name.
+
$name
+
The method name.
+
$closure
+
The new code for the method.
+
If null is passed, the existing method body will be copied.
+
$visibility
+
Optional. The method visibility, one of "public", "protected", or "private".
+
If null is passed, the existing visibility will be preserved.
+
$static
+
Optional. Whether or not the method should be static. Default is false.
+
If null is passed, the existing state will be used.
+
+ +#### Return values + +This method will return the calling class, enabling multiple methods to be chained. + +An `AssertWell\PHPUnitGlobalState\Exceptions\RunkitException` will be thrown if the given method cannot be (re)defined. + +--- + +### deleteMethod() + +Delete/undefine a method for the duration of the single test. + +`deleteMethod(string $class, string $name): self` + +#### Parameters + +
+
$class
+
The class name.
+
$name
+
The method name.
+
+ +#### Return values + +This method will return the calling class, enabling multiple methods to be chained. diff --git a/src/Exceptions/MethodExistsException.php b/src/Exceptions/MethodExistsException.php new file mode 100644 index 0000000..2ad5257 --- /dev/null +++ b/src/Exceptions/MethodExistsException.php @@ -0,0 +1,8 @@ + [], + 'redefined' => [], + ]; + + /** + * @after + * + * @return void + */ + protected function restoreMethods() + { + // Reset anything that was modified. + array_walk($this->methods['redefined'], function ($methods, $class) { + foreach ($methods as $modified => $original) { + if (method_exists($class, $modified)) { + Runkit::method_remove($class, $modified); + } + + // Put the original back into place. + Runkit::method_rename($class, $original, $modified); + } + + unset($this->methods['redefined'][$class]); + }); + + array_walk($this->methods['defined'], function ($methods, $class) { + foreach ($methods as $method) { + Runkit::method_remove($class, $method); + } + unset($this->methods['defined'][$class]); + }); + + Runkit::reset(); + } + + /** + * Define a new method. + * + * @throws \AssertWell\PHPUnitGlobalState\Exceptions\MethodExistsException + * @throws \AssertWell\PHPUnitGlobalState\Exceptions\RunkitException + * + * @param string $class The class name. + * @param string $name The method name. + * @param \Closure $closure The method body. + * @param string $visibility Optional. The method visibility, one of "public", "protected", + * or "private". Default is "public". + * @param bool $static Optional. Whether or not the method should be defined as static. + * Default is false. + * + * @return self + */ + protected function defineMethod($class, $name, \Closure $closure, $visibility = 'public', $static = false) + { + if (method_exists($class, $name)) { + throw new MethodExistsException(sprintf( + 'Method %1$s::%2$s() already exists. You may redefine it using %3$s::redefineMethod() instead.', + $class, + $name, + get_class($this) + )); + } + + if (! Runkit::isAvailable()) { + $this->markTestSkipped('defineMethod() requires Runkit be available, skipping.'); + } + + $flags = Runkit::getVisibilityFlags($visibility, $static); + + if (! Runkit::method_add($class, $name, $closure, $flags)) { + throw new RunkitException(sprintf('Unable to define method %1$s::%2$s().', $class, $name)); + } + + if (! isset($this->methods['defined'][$class])) { + $this->methods['defined'][$class] = []; + } + $this->methods['defined'][$class][] = $name; + + return $this; + } + + /** + * Redefine an existing method. + * + * If the method doesn't yet exist, it will be defined. + * + * @param string $class The class name. + * @param string $name The method name. + * @param \Closure|null $closure Optional. A closure representing the method body. If null, + * the method body will not be replaced. Default is null. + * @param string $visibility Optional. The method visibility, one of "public", + * "protected", or "private". Default is the same as the + * current value. + * @param bool $static Optional. Whether or not the method should be defined as + * static. Default is the same is as the current value. + * + * @return self + */ + protected function redefineMethod($class, $name, $closure = null, $visibility = null, $static = null) + { + if (! method_exists($class, $name)) { + if (! $closure instanceof \Closure) { + throw new RunkitException( + sprintf('New method %1$s::$2$s() cannot have an empty body.', $class, $name) + ); + } + + return $this->defineMethod($class, $name, $closure, $visibility, $static); + } + + if (! Runkit::isAvailable()) { + $this->markTestSkipped('redefineMethod() requires Runkit be available, skipping.'); + } + + $method = new \ReflectionMethod($class, $name); + + if (null === $visibility) { + if ($method->isPrivate()) { + $visibility = 'private'; + } elseif ($method->isProtected()) { + $visibility = 'protected'; + } else { + $visibility = 'public'; + } + } + + if (null === $static) { + $static = $method->isStatic(); + } + + $flags = Runkit::getVisibilityFlags($visibility, $static); + + // If $closure is null, copy the existing method body. + if (null === $closure) { + $closure = $method->isStatic() + ? $method->getClosure() + : $method->getClosure($this->getMockBuilder($class) + ->disableOriginalConstructor() + ->getMock()); + } + + // Back up the original version of the method. + if (! isset($this->methods['redefined'][$class][$name])) { + $prefixed = Runkit::makePrefixed($name); + + if (! Runkit::method_rename($class, $name, $prefixed)) { + throw new RunkitException( + sprintf('Unable to back up %1$s::%2$s(), aborting.', $class, $name) + ); + } + + if (! isset($this->methods['redefined'][$class])) { + $this->methods['redefined'][$class] = []; + } + $this->methods['redefined'][$class][$name] = $prefixed; + + if (! Runkit::method_add($class, $name, $closure, $flags)) { + throw new RunkitException( + sprintf('Unable to redefine function %1$s::%2$s().', $method, $name) + ); + } + } else { + Runkit::method_redefine($class, $name, $closure, $flags); + } + + return $this; + } + + /** + * Delete an existing method. + * + * @param string $class The class name. + * @param string $name The method to be deleted. + * + * @return self + */ + protected function deleteMethod($class, $name) + { + if (! method_exists($class, $name)) { + return $this; + } + + $prefixed = Runkit::makePrefixed($name); + + if (! Runkit::method_rename($class, $name, $prefixed)) { + throw new RunkitException( + sprintf('Unable to back up %1$s::%2$s(), aborting.', $class, $name) + ); + } + + if (! isset($this->methods['redefined'][$class])) { + $this->methods['redefined'][$class] = []; + } + $this->methods['redefined'][$class][$name] = $prefixed; + + return $this; + } +} diff --git a/src/Support/Runkit.php b/src/Support/Runkit.php index eb3fa69..9cc7f3b 100644 --- a/src/Support/Runkit.php +++ b/src/Support/Runkit.php @@ -21,8 +21,10 @@ * @method static bool function_rename(string $funcname, string $newname) * @method static bool import(string $filename, int $flags = NULL) * @method static bool method_add(string $classname, string $methodname, string $args, string $code, int $flags = RUNKIT7_ACC_PUBLIC, string $doc_comment = NULL, string $return_type = NULL, bool $is_strict = NULL) + * @method static bool method_add(string $classname, string $methodname, \Closure $closure, int $flags = RUNKIT7_ACC_PUBLIC, string $doc_comment = NULL, string $return_type = NULL, bool $is_strict = NULL) * @method static bool method_copy(string $dClass, string $dMethod, string $sClass, string $sMethod = NULL) * @method static bool method_redefine(string $classname, string $methodname, string $args, string $code, int $flags = RUNKIT7_ACC_PUBLIC, string $doc_comment = NULL, string $return_type, bool $is_strict = NULL) + * @method static bool method_redefine(string $classname, string $methodname, \Closure $closure, int $flags = RUNKIT7_ACC_PUBLIC, string $doc_comment = NULL, string $return_type, bool $is_strict = NULL) * @method static bool method_remove(string $classname, string $methodname) * @method static bool method_rename(string $classname, string $methodname, string $newname) * @method static int object_id(object $obj) @@ -39,6 +41,13 @@ class Runkit */ private static $namespace; + /** + * A prefix used to move things out of the way for the duration of a test. + * + * @var string + */ + private static $prefix; + /** * Dynamically alias methods to the underlying Runkit functions. * @@ -93,6 +102,57 @@ public static function getNamespace() return self::$namespace; } + /** + * Get the current runkit prefix. + * + * If the property is currently empty, one will be created. + * + * @return string The prefix we're applying to renamed methods. + */ + public static function getPrefix() + { + if (empty(self::$prefix)) { + self::$prefix = str_replace('\\', '_', self::getNamespace()); + } + + return self::$prefix; + } + + /** + * Get the appropriate visibility/static flag(s) for defining methods. + * + * @param string $visibility The method visibility. + * @param bool $static Optional. Whether or not the method should be defined as static. + * Default is false. + * + * @return int The corresponding visibility flag, possibly combined with RUNKIT7_ACC_STATIC + * depending on $static. + */ + public static function getVisibilityFlags($visibility, $static = false) + { + if ('protected' === $visibility) { + if (defined('RUNKIT7_ACC_PROTECTED')) { + return $static ? RUNKIT7_ACC_PROTECTED | RUNKIT7_ACC_STATIC : RUNKIT7_ACC_PROTECTED; + } + + return $static ? RUNKIT_ACC_PROTECTED | RUNKIT_ACC_STATIC : RUNKIT_ACC_PROTECTED; + } + + if ('private' === $visibility) { + if (defined('RUNKIT7_ACC_PRIVATE')) { + return $static ? RUNKIT7_ACC_PRIVATE | RUNKIT7_ACC_STATIC : RUNKIT7_ACC_PRIVATE; + } + + return $static ? RUNKIT_ACC_PRIVATE | RUNKIT_ACC_STATIC : RUNKIT_ACC_PRIVATE; + } + + if (defined('RUNKIT7_ACC_PUBLIC')) { + return $static ? RUNKIT7_ACC_PUBLIC | RUNKIT7_ACC_STATIC : RUNKIT7_ACC_PUBLIC; + } + + return $static ? RUNKIT_ACC_PUBLIC | RUNKIT_ACC_STATIC : RUNKIT_ACC_PUBLIC; + } + /** * Namespace the given reference. * @@ -110,15 +170,28 @@ public static function makeNamespaced($var) return self::getNamespace() . $var; } + /** + * Prefix the given reference. + * + * @param string $var The item to be given the temporary test prefix. + * + * @return string The newly-namespaced item. + */ + public static function makePrefixed($var) + { + return self::getPrefix() . str_replace('\\', '_', $var); + } + /** * Reset static properties. * - * This is helpful to run before tests in case self::$namespace gets polluted. + * This is helpful to run before tests in case self::$namespace or self::$prefix get polluted. * * @return void */ public static function reset() { self::$namespace = ''; + self::$prefix = ''; } } diff --git a/tests/MethodsTest.php b/tests/MethodsTest.php new file mode 100644 index 0000000..8b1f58e --- /dev/null +++ b/tests/MethodsTest.php @@ -0,0 +1,399 @@ +markTestSkipped('This test depends on runkit being available.'); + } + } + + /** + * @test + * @testdox defineMethod() should be able to define a new method + */ + public function defineMethod_should_be_able_to_define_a_new_method() + { + $this->assertFalse(method_exists(TestClass::class, 'myMethod')); + + $this->defineMethod(TestClass::class, 'myMethod', function ($return) { + return $return; + }); + + $this->assertSame(123, (new TestClass())->myMethod(123)); + + $this->restoreMethods(); + $this->assertFalse( + method_exists(TestClass::class, 'myMethod'), + 'The new method should have been undefined.' + ); + } + + /** + * @test + * @testdox defineMethod() should be able to set the visibility and static properties + * @dataProvider provideVisibilityStaticCombinations + * @depends defineMethod_should_be_able_to_define_a_new_method + * + * @param string $visibility The visibility to apply. + * @param bool $static Whether or not to make the method static. + */ + public function defineMethod_should_be_able_to_set_visibility_and_static($visibility, $static) + { + $this->defineMethod(TestClass::class, 'myMethod', function ($return) { + return $return; + }, $visibility, $static); + + $method = new \ReflectionMethod(TestClass::class, 'myMethod'); + $visibilityMethod = sprintf('is%s', ucwords($visibility)); + + $this->assertTrue( + $method->$visibilityMethod(), + sprintf('Expected method to be %s.', $visibility) + ); + $this->assertSame( + $static, + $method->isStatic(), + $static ? 'Expected method to be static' : 'Expected method to not be static' + ); + } + + /** + * @test + * @testdox defineMethod() should throw a warning if the method already exists + */ + public function defineMethod_should_throw_a_warning_if_the_method_already_exists() + { + $this->assertTrue(method_exists(TestClass::class, 'publicMethod')); + $signature = (string) (new \ReflectionMethod(TestClass::class, 'publicMethod')); + + $this->expectException(MethodExistsException::class); + $this->defineMethod(TestClass::class, 'publicMethod', function ($return) { + return $return; + }); + + $this->assertSame( + $signature, + (string) (new \ReflectionMethod(TestClass::class, 'publicMethod')), + 'The original method should have been left untouched.' + ); + } + + /** + * @test + * @testdox redefineMethod() should be able to redefine an existing method + */ + public function redefineMethod_should_be_able_to_redefine_existing_methods() + { + $this->assertTrue(method_exists(TestClass::class, 'publicMethod')); + $signature = (string) (new \ReflectionMethod(TestClass::class, 'publicMethod')); + + $this->redefineMethod(TestClass::class, 'publicMethod', function () { + return 123; + }); + + $this->assertSame(123, (new TestClass())->publicMethod('some string')); + + $this->restoreMethods(); + $this->assertTrue(method_exists(TestClass::class, 'publicMethod')); + $this->assertSame( + $signature, + (string) (new \ReflectionMethod(TestClass::class, 'publicMethod')), + 'The original method definition should have been restored.' + ); + } + + /** + * @test + * @testdox redefineMethod() should be able to change method visibility + * @depends redefineMethod_should_be_able_to_redefine_existing_methods + */ + public function redefineMethod_should_be_able_to_change_method_visibility() + { + $this->assertTrue(method_exists(TestClass::class, 'publicMethod')); + $signature = (string) (new \ReflectionMethod(TestClass::class, 'publicMethod')); + + $this->redefineMethod(TestClass::class, 'publicMethod', function () { + return 123; + }, 'protected'); + + $method = new \ReflectionMethod(TestClass::class, 'publicMethod'); + $method->setAccessible(true); + + $this->assertSame(123, $method->invoke(new TestClass())); + $this->assertTrue($method->isProtected()); + + $this->restoreMethods(); + $this->assertSame( + $signature, + (string) (new \ReflectionMethod(TestClass::class, 'publicMethod')), + 'The original method definition should have been restored.' + ); + $this->assertTrue((new \ReflectionMethod(TestClass::class, 'publicMethod'))->isPublic()); + } + + /** + * @test + * @testdox redefineMethod() should be preserve visibility by default + * @depends redefineMethod_should_be_able_to_redefine_existing_methods + */ + public function redefineMethod_should_preserve_visibility() + { + $this->assertTrue(method_exists(TestClass::class, 'protectedMethod')); + $signature = (string) (new \ReflectionMethod(TestClass::class, 'protectedMethod')); + + $this->redefineMethod(TestClass::class, 'protectedMethod', function () { + return 123; + }); + + $method = new \ReflectionMethod(TestClass::class, 'protectedMethod'); + $this->assertTrue($method->isProtected()); + } + + /** + * @test + * @testdox redefineMethod() should be able to make a method static + * @depends redefineMethod_should_be_able_to_redefine_existing_methods + */ + public function redefineMethod_should_be_able_to_make_a_method_static() + { + $this->assertTrue(method_exists(TestClass::class, 'publicMethod')); + $signature = (string) (new \ReflectionMethod(TestClass::class, 'publicMethod')); + + $this->redefineMethod(TestClass::class, 'publicMethod', function () { + return 123; + }, 'public', true); + + $this->assertTrue((new \ReflectionMethod(TestClass::class, 'publicMethod'))->isStatic()); + $this->assertSame(123, TestClass::publicMethod()); + + $this->restoreMethods(); + $this->assertSame( + $signature, + (string) (new \ReflectionMethod(TestClass::class, 'publicMethod')), + 'The original method definition should have been restored.' + ); + $this->assertFalse((new \ReflectionMethod(TestClass::class, 'publicMethod'))->isStatic()); + } + + /** + * @test + * @testdox redefineMethod() should be preserve static state by default + * @depends redefineMethod_should_be_able_to_redefine_existing_methods + */ + public function redefineMethod_should_preserve_static() + { + $this->assertTrue(method_exists(TestClass::class, 'protectedStaticMethod')); + $signature = (string) (new \ReflectionMethod(TestClass::class, 'protectedStaticMethod')); + + $this->redefineMethod(TestClass::class, 'protectedStaticMethod', function () { + return 123; + }); + + $method = new \ReflectionMethod(TestClass::class, 'protectedStaticMethod'); + $this->assertTrue($method->isStatic()); + } + + /** + * @test + * @testdox Passing a null body to redefineMethod() should leave the original signature + * @depends redefineMethod_should_be_able_to_redefine_existing_methods + */ + public function redefineMethod_can_accept_a_null_body() + { + $this->assertTrue(method_exists(TestClass::class, 'publicMethod')); + $signature = (string) (new \ReflectionMethod(TestClass::class, 'publicMethod')); + + $this->redefineMethod(TestClass::class, 'publicMethod', null, 'protected', true); + + $method = new \ReflectionMethod(TestClass::class, 'publicMethod'); + $this->assertTrue($method->isProtected(), 'The visibility changes should have been applied.'); + $this->assertTrue($method->isStatic(), 'The static changes should have been applied.'); + } + + /** + * @test + * @testdox redefineMethod() should be able to make a method non-static + * @depends redefineMethod_should_be_able_to_redefine_existing_methods + */ + public function redefineMethod_should_be_able_to_make_a_method_nonstatic() + { + $this->assertTrue(method_exists(TestClass::class, 'publicStaticMethod')); + $signature = (string) (new \ReflectionMethod(TestClass::class, 'publicStaticMethod')); + + $this->redefineMethod(TestClass::class, 'publicStaticMethod', function () { + return 123; + }, 'public', false); + + $this->assertFalse((new \ReflectionMethod(TestClass::class, 'publicStaticMethod'))->isStatic()); + $this->assertSame(123, (new TestClass())->publicStaticMethod()); + + $this->restoreMethods(); + $this->assertSame( + $signature, + (string) (new \ReflectionMethod(TestClass::class, 'publicStaticMethod')), + 'The original method definition should have been restored.' + ); + $this->assertTrue((new \ReflectionMethod(TestClass::class, 'publicStaticMethod'))->isStatic()); + } + + /** + * @test + * @testdox redefineMethod() should be able to redefine newly-defined methods + */ + public function redefineMethod_should_be_able_to_redefine_newly_defined_methods() + { + $this->defineMethod(TestClass::class, 'myMethod', function () { + return 'abc'; + }); + $this->redefineMethod(TestClass::class, 'myMethod', function () { + return 'xyz'; + }); + + $this->assertSame('xyz', (new TestClass())->myMethod()); + + $this->restoreMethods(); + $this->assertFalse( + method_exists(TestClass::class, 'myMethod'), + 'The newly-created method should still be removed.' + ); + } + + /** + * @test + * @testdox redefineMethod() should be able to redefine an existing methods multiple times + * @depends redefineMethod_should_be_able_to_redefine_existing_methods + */ + public function redefineMethod_should_be_able_to_redefine_existing_methods_multiple_times() + { + $this->assertTrue(method_exists(TestClass::class, 'publicMethod')); + $signature = (string) (new \ReflectionMethod(TestClass::class, 'publicMethod')); + + $this->redefineMethod(TestClass::class, 'publicMethod', function () { + return 'first'; + }); + $this->redefineMethod(TestClass::class, 'publicMethod', function () { + return 'second'; + }); + $this->redefineMethod(TestClass::class, 'publicMethod', function () { + return 'third'; + }); + + $this->assertSame( + 'third', + (new TestClass())->publicMethod(), + 'Expected the latest re-definition to be used.' + ); + + $this->restoreMethods(); + $this->assertSame( + $signature, + (string) (new \ReflectionMethod(TestClass::class, 'publicMethod')), + 'The original method definition should have been restored.' + ); + } + + /** + * @test + * @testdox redefineMethod() should define methods if they do not exist + * @depends redefineMethod_should_be_able_to_redefine_existing_methods + */ + public function redefineMethod_should_define_methods_if_they_do_not_exist() + { + $this->assertFalse(method_exists(TestClass::class, 'myMethod')); + + + $this->redefineMethod(TestClass::class, 'myMethod', function () { + return 'value'; + }); + + $this->assertSame('value', (new TestClass())->myMethod()); + + $this->restoreMethods(); + $this->assertFalse( + method_exists(TestClass::class, 'myMethod'), + 'The new method should have been undefined.' + ); + } + + /** + * @test + * @testdox deleteMethod() should be able to delete methods + */ + public function deleteMethod_should_be_able_to_delete_methods() + { + $this->assertTrue( + method_exists(TestClass::class, 'publicMethod'), + 'Test is predicated on this method existing.' + ); + + $this->deleteMethod(TestClass::class, 'publicMethod'); + $this->assertFalse( + method_exists(TestClass::class, 'publicMethod'), + 'The method should have been deleted.' + ); + + $this->restoreMethods(); + $this->assertTrue( + method_exists(TestClass::class, 'publicMethod'), + 'The method should have been restored.' + ); + } + + /** + * @test + * @testdox deleteMethod() should do nothing if the method does not exist + */ + public function deleteMethod_should_do_nothing_if_the_method_does_not_exist() + { + $this->assertFalse( + method_exists(TestClass::class, 'someFakeMethod'), + 'Test is predicated on this method NOT existing.' + ); + + $this->deleteMethod(TestClass::class, 'someFakeMethod'); + + $this->assertFalse( + method_exists(TestClass::class, 'someFakeMethod'), + 'Deleting a non-existent method should not do anything.' + ); + + $this->restoreMethods(); + $this->assertFalse( + method_exists(TestClass::class, 'someFakeMethod'), + 'Nothing should be restored as there was nothing to begin with.' + ); + } + + /** + * Provide combinations of visibility and static. + * + * @return array[] + */ + public function provideVisibilityStaticCombinations() + { + return [ + 'Public, non-static' => ['public', false], + 'Protected, non-static' => ['protected', false], + 'Private, non-static' => ['private', false], + 'Public, static' => ['public', true], + 'Protected, static' => ['protected', true], + 'Private, static' => ['private', true], + ]; + } +} diff --git a/tests/Support/RunkitTest.php b/tests/Support/RunkitTest.php index b9474cf..6187271 100644 --- a/tests/Support/RunkitTest.php +++ b/tests/Support/RunkitTest.php @@ -56,4 +56,22 @@ public function makeNamespaced_should_return_the_given_reference_with_a_prefixed 'Leading slashes should be stripped.' ); } + + /** + * @test + */ + public function makePrefixed_should_return_the_given_reference_with_a_prefix() + { + $prefix = Runkit::getPrefix(); + + $this->assertSame( + $prefix . 'some_method', + Runkit::makePrefixed('some_method') + ); + $this->assertSame( + $prefix . 'Some_Namespaced_function_to_move', + Runkit::makePrefixed('Some\\Namespaced\\function_to_move'), + 'Namespaces should be preserved.' + ); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index be7fcb5..2630165 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,6 +7,7 @@ use AssertWell\PHPUnitGlobalState\EnvironmentVariables; use AssertWell\PHPUnitGlobalState\Functions; use AssertWell\PHPUnitGlobalState\GlobalVariables; +use AssertWell\PHPUnitGlobalState\Methods; /** * Since this test suite is testing a series of traits meant to aid in testing other codebases @@ -21,4 +22,5 @@ abstract class TestCase extends BaseTestCase use EnvironmentVariables; use Functions; use GlobalVariables; + use Methods; } diff --git a/tests/stubs/TestClass.php b/tests/stubs/TestClass.php new file mode 100644 index 0000000..7a794c8 --- /dev/null +++ b/tests/stubs/TestClass.php @@ -0,0 +1,66 @@ +