diff --git a/php/WP_Mock.php b/php/WP_Mock.php index 49ef1f3..1f24249 100644 --- a/php/WP_Mock.php +++ b/php/WP_Mock.php @@ -32,6 +32,19 @@ class WP_Mock protected static $__strict_mode = false; + /** + * A record if strict mode was set individually for this test. + * + * Uses an associative array containing both method and setting as test-method-string:is-enabled-bool. + * + * @used-by self::setStrictModeForTest() + * @used-by self::isStrictModeForTest() + * @see self::strictMode() + * + * @var array + */ + protected static $__strict_mode_for_individual_test = []; + /** @var DeprecatedMethodListener */ protected static $deprecatedMethodListener; @@ -57,7 +70,7 @@ public static function usingPatchwork() */ public static function strictMode() { - return (bool) self::$__strict_mode; + return self::isStrictModeForTest() ?? (bool) self::$__strict_mode; } /** @@ -70,6 +83,72 @@ public static function activateStrictMode() } } + /** + * Sets strict mode on or off at runtime for an individual test. + * + * Records the config/preference for the individual test. Later this will be preferred over the default. + * + * @param bool $enabled + * @throws Exception when the test case name cannot be determined. + */ + public static function setStrictModeForTest(bool $enabled = true): void + { + $currentTestName = self::getCurrentlyRunningTestName(); + if(is_null($currentTestName)){ + throw new Exception('Failed to determine current test name'); + } + self::$__strict_mode_for_individual_test = [$currentTestName => $enabled,]; + } + + /** + * Check was strict mode configured individually for this test case. + * + * @see self::setStrictModeForTest() + * @see self::$__strict_mode_for_individual_test + * + * @return ?bool `null` when not set, boolean preference when set. + */ + protected static function isStrictModeForTest(): ?bool { + if( empty( self::$__strict_mode_for_individual_test ) ) { + return null; + } + + $currentTestName = self::getCurrentlyRunningTestName(); + + if(!is_null($currentTestName) && isset(self::$__strict_mode_for_individual_test[$currentTestName])) { + return self::$__strict_mode_for_individual_test[$currentTestName]; + } + + // Reset the array since it is only relevant for the current test case run. + self::$__strict_mode_for_individual_test = []; + + return null; + } + + /** + * Perform a backtrace to determine the currently running test. + * + * @return ?string of `class-string::method` + */ + protected static function getCurrentlyRunningTestName(): ?string { + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + + /** @var array{class?:string, function?:string} $trace */ + foreach ($backtrace as $trace) { + if (isset($trace['class']) && isset($trace['function'])) { + // Check if this is a PHPUnit test class + if (is_subclass_of($trace['class'], \PHPUnit\Framework\TestCase::class)) { + // Test method names start with 'test' or have @test annotation + if (strpos($trace['function'], 'test') === 0) { + return $trace['class'] . '::' . $trace['function']; + } + } + } + } + + return null; + } + /** * Bootstraps WP_Mock. * diff --git a/tests/Integration/WP_MockTest.php b/tests/Integration/WP_MockTest.php index 3af7bb3..c441102 100644 --- a/tests/Integration/WP_MockTest.php +++ b/tests/Integration/WP_MockTest.php @@ -44,6 +44,18 @@ class WP_MockTest extends WP_MockTestCase */ protected function setUp(): void { + /** + * Reset to default strict-mode after tests that manipulate this value. + * + * @see WP_Mock::$__strict_mode + */ + $property = new \ReflectionProperty( \WP_Mock::class, '__strict_mode' ); + // "Method ReflectionProperty::setAccessible() is deprecated since 8.5, as it has no effect". + if(!version_compare(PHP_VERSION, '8.5', '>=')) { + $property->setAccessible( true ); + } + $property->setValue( null, false ); + if (! $this->isInIsolation()) { WP_Mock::setUp(); } @@ -335,4 +347,63 @@ public function testCanExpectHooksNotAdded() : void $this->assertConditionsMet(); } + + /** + * @covers \WP_Mock::setStrictModeForTest() + * + * @return void + * @throws Exception + */ + public function testCanDisableStrictModeForLegacyCode() : void + { + // Set default ala `WP_Mock::activateStrictMode()`. + $property = new \ReflectionProperty( WP_Mock::class, '__strict_mode' ); + if(!version_compare(PHP_VERSION, '8.5', '>=')) { + $property->setAccessible( true ); + } + $property->setValue( null, true ); + + // Temporarily disable strict mode to test legacy code + WP_Mock::setStrictModeForTest(false); + + // This would normally fail in strict mode, but should pass now + add_action('legacy_action', 'legacy_callback'); + + $this->assertTrue(true); + } + + /** + * @covers \WP_Mock::setStrictModeForTest() + * + * @runInSeparateProcess + * @preserveGlobalState disabled + * + * @return void + * @throws Exception|ExpectationFailedException + */ + public function testCanEnableStrictModeForSpecificTest() : void + { + /** + * Set default ala `WP_Mock::activateStrictMode()`, `false`. + * @see \WP_Mock::$__strict_mode + */ + $property = new \ReflectionProperty( \WP_Mock::class, '__strict_mode' ); + if(!version_compare(PHP_VERSION, '8.5', '>=')) { + $property->setAccessible( true ); + } + $property->setValue( null, false ); + + $this->assertFalse(WP_Mock::strictMode()); + + // Enable strict mode for this specific test + WP_Mock::setStrictModeForTest(); + $this->assertTrue(WP_Mock::strictMode()); + + // This should throw an exception because we haven't set expectations + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('No handler found for function unmocked_function'); + + // Call an unmocked function + WP_Mock\Functions\Handler::handleFunction('unmocked_function'); + } } diff --git a/tests/Unit/WP_MockTest.php b/tests/Unit/WP_MockTest.php index 82f34c0..edc7b63 100644 --- a/tests/Unit/WP_MockTest.php +++ b/tests/Unit/WP_MockTest.php @@ -68,6 +68,66 @@ public function testActivateStrictModeDoesNotWorkAfterBootstrap(): void $this->assertFalse(WP_Mock::strictMode()); } + /** + * @covers \WP_Mock::setStrictModeForTest() + * + * @return void + * @throws \Exception + */ + public function testSetStrictModeForTestCanDisableStrictMode(): void + { + // Set default ala `WP_Mock::activateStrictMode()`. + $property = new \ReflectionProperty( WP_Mock::class, '__strict_mode' ); + $property->setAccessible( true ); + $property->setValue( null, true ); + + + WP_Mock::setStrictModeForTest(false); + + $this->assertFalse(WP_Mock::strictMode()); + } + + /** + * @covers \WP_Mock::setStrictModeForTest() + * + * @return void + * @throws \Exception + */ + public function testSetStrictModeForTestCanEnableStrictMode(): void + { + // Set default ala `WP_Mock::activateStrictMode()`, but `false`. + $property = new \ReflectionProperty( WP_Mock::class, '__strict_mode' ); + $property->setAccessible( true ); + $property->setValue( null, false ); + + WP_Mock::setStrictModeForTest(); + + $this->assertTrue(WP_Mock::strictMode()); + } + + /** + * @covers \WP_Mock::setStrictModeForTest() + * + * @return void + * @throws ExpectationFailedException|InvalidArgumentException + */ + public function testPreviousSetStrictModeForTestIsNotRelevant(): void + { + // Set default ala `WP_Mock::activateStrictMode()`. + $property = new \ReflectionProperty( WP_Mock::class, '__strict_mode' ); + $property->setAccessible( true ); + $property->setValue( null, true ); + + // Set individual test configuration for another tests. + $property = new \ReflectionProperty( WP_Mock::class, '__strict_mode_for_individual_test' ); + $property->setAccessible( true ); + $property->setValue( null, [ + 'WP_Mock\Tests\Unit\WP_MockTest::testSetStrictModeForTestCanDisableStrictMode' => false + ]); + + $this->assertTrue(WP_Mock::strictMode()); + } + /** * @covers \WP_Mock::userFunction() *