diff --git a/.upgradenotes/MDL-66903-2024070502035383.yml b/.upgradenotes/MDL-66903-2024070502035383.yml new file mode 100644 index 00000000000..255dd885601 --- /dev/null +++ b/.upgradenotes/MDL-66903-2024070502035383.yml @@ -0,0 +1,10 @@ +issueNumber: MDL-66903 +notes: + core: + - message: > + Added the ability for unit tests to autoload classes in the + `\[component]\tests\` + + namespace from the `[path/to/component]/tests/classes` directory. + type: improved + diff --git a/lib/classes/component.php b/lib/classes/component.php index 0c6063c9eca..a33117f09f9 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -172,6 +172,38 @@ class core_component { require($file); return; } + + if (PHPUNIT_TEST) { + // For unit tests we support classes in `\frankenstyle_component\tests\` to be loaded from + // `path/to/frankenstyle/component/tests/classes` directory. + // Note: We do *not* support the legacy `\frankenstyle_component_tests_style_classnames`. + if ($component = self::get_component_from_classname($classname)) { + $pathoptions = [ + '/tests/classes' => "{$component}\\tests\\", + '/tests/behat' => "{$component}\\behat\\", + ]; + foreach ($pathoptions as $path => $testnamespace) { + if (preg_match("#^" . preg_quote($testnamespace) . "#", $classname)) { + $path = self::get_component_directory($component) . $path; + $relativeclassname = str_replace( + $testnamespace, + '', + $classname, + ); + $file = sprintf( + "%s/%s.php", + $path, + str_replace('\\', '/', $relativeclassname), + ); + if (!empty($file) && file_exists($file)) { + require($file); + return; + } + break; + } + } + } + } } /** diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php index 0e0bb2d1cb6..1ad2fde17a6 100644 --- a/lib/phpunit/classes/util.php +++ b/lib/phpunit/classes/util.php @@ -529,6 +529,7 @@ class phpunit_util extends testing_util { $template = << @dir@ + @dir@/classes EOF; @@ -621,6 +622,7 @@ class phpunit_util extends testing_util { . + ./classes EOT; diff --git a/lib/tests/component_test.php b/lib/tests/component_test.php index 98ca138454d..894a3e05227 100644 --- a/lib/tests/component_test.php +++ b/lib/tests/component_test.php @@ -914,6 +914,69 @@ final class component_test extends advanced_testcase { ]; } + /** + * Test that the classloader can load from the test namespaces. + */ + public function test_classloader_tests_namespace(): void { + global $CFG; + + $this->resetAfterTest(); + + $getclassfilecontent = function (string $classname, ?string $namespace): string { + if ($namespace) { + $content = " [ + 'classes' => [ + 'example.php' => $getclassfilecontent('example', 'core'), + ], + 'tests' => [ + 'classes' => [ + 'example_classname.php' => $getclassfilecontent('example_classname', \core\tests::class), + ], + 'behat' => [ + 'example_classname.php' => $getclassfilecontent('example_classname', \core\behat::class), + ], + ], + ], + ]); + + // Note: This is pretty hacky, but it's the only way to test the classloader. + // We have to override the dirroot and libdir, and then reset the plugintypes property. + $CFG->dirroot = $vfileroot->url(); + $CFG->libdir = $vfileroot->url() . '/lib'; + (new ReflectionProperty('core_component', 'plugintypes'))->setValue(null, null); + + // Existing classes do not break. + $this->assertTrue( + class_exists(\core\example::class), + ); + + // Test and behat classes work. + $this->assertTrue( + class_exists(\core\tests\example_classname::class), + ); + $this->assertTrue( + class_exists(\core\behat\example_classname::class), + ); + + // Non-existent classes do not do anything. + $this->assertFalse( + class_exists(\core\tests\example_classname_not_found::class), + ); + } + + public function tearDown(): void { + $plugintypes = new ReflectionProperty('core_component', 'plugintypes'); + $plugintypes->setValue(null, null); + } + /** * Test the PSR classloader. * diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fd7faeecc08..de77041fcf2 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -35,54 +35,74 @@ lib/phpunit/tests + lib/phpunit/tests/classes lib/testing/tests + lib/testing/tests/classes lib/ddl/tests + lib/ddl/tests/classes lib/dml/tests + lib/dml/tests/classes lib/tests + lib/tests/classes lib/external/tests + lib/external/tests/classes favourites/tests + favourites/tests/classes lib/form/tests + lib/form/tests/classes lib/filestorage/tests lib/filebrowser/tests files/tests + lib/filestorage/tests/classes + lib/filebrowser/tests/classes + files/tests/classes filter/tests + filter/tests/classes admin/roles/tests + admin/roles/tests/classes cohort/tests + cohort/tests/classes lib/grade/tests grade/tests grade/grading/tests grade/import/csv/tests + lib/grade/tests/classes + grade/tests/classes + grade/grading/tests/classes + grade/import/csv/tests/classes analytics/tests + analytics/tests/classes availability/tests + availability/tests/classes backup/controller/tests @@ -90,135 +110,185 @@ backup/moodle2/tests backup/tests backup/util + backup/controller/tests/classes + backup/converter/moodle1/tests/classes + backup/moodle2/tests/classes + backup/tests/classes + backup/util/classes badges/tests + badges/tests/classes blog/tests + blog/tests/classes customfield/tests + customfield/tests/classes iplookup/tests + iplookup/tests/classes course/tests + course/tests/classes course/format/tests + course/format/tests/classes privacy/tests + privacy/tests/classes question/engine/tests question/tests question/type/tests question/engine/upgrade/tests + question/engine/tests/classes + question/tests/classes + question/type/tests/classes + question/engine/upgrade/tests/classes cache/tests + cache/tests/classes calendar/tests + calendar/tests/classes enrol/tests + enrol/tests/classes group/tests + group/tests/classes message/tests + message/tests/classes notes/tests + notes/tests/classes tag/tests + tag/tests/classes rating/tests + rating/tests/classes repository/tests + repository/tests/classes lib/userkey/tests + lib/userkey/tests/classes user/tests + user/tests/classes webservice/tests + webservice/tests/classes mnet/tests + mnet/tests/classes completion/tests + completion/tests/classes comment/tests + comment/tests/classes search/tests + search/tests/classes competency/tests + competency/tests/classes my/tests + my/tests/classes auth/tests + auth/tests/classes blocks/tests + blocks/tests/classes login/tests + login/tests/classes plagiarism/tests + plagiarism/tests/classes portfolio/tests + portfolio/tests/classes lib/editor/tests + lib/editor/tests/classes rss/tests + rss/tests/classes lib/table/tests + lib/table/tests/classes h5p/tests + h5p/tests/classes lib/xapi/tests + lib/xapi/tests/classes contentbank/tests + contentbank/tests/classes payment/tests + payment/tests/classes reportbuilder/tests + reportbuilder/tests/classes admin/presets/tests + admin/presets/tests/classes admin/tests + admin/tests/classes communication/tests + communication/tests/classes