Moodle phpunit testing
Skill SaadRahman01/claude-moodle-dev/skills/moodle-phpunit-testing
Use when writing, running, or debugging PHPUnit tests for Moodle plugins or core. Covers advanced_testcase, resetAfterTest, data generators, mocking $DB, testing events/tasks/external functions, and CLI invocation.From its SKILL.md
npx -y skills add SaadRahman01/claude-moodle-dev --skill moodle-phpunit-testingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
6.7 KB, as published. Nobody here has run it
Moodle PHPUnit Testing
Overview
Moodle ships its own PHPUnit harness with test bootstrap, transactional resets, and data generators. Tests live in <plugin>/tests/<thing>_test.php and extend advanced_testcase. Never call parent::setUp() for DB cleanup — use $this->resetAfterTest().
When to Use
- Writing unit/integration tests for any Moodle plugin or core API
- Debugging test failures (
Database was modifiederrors, isolation issues) - Adding a data generator (
tests/generator/lib.php) - Testing events, scheduled tasks, ad-hoc tasks, external functions
- Setting up CI for Moodle test suites
Skip when: writing Behat acceptance tests (use moodle-behat-testing).
First-time setup
php admin/tool/phpunit/cli/init.php # writes phpunit.xml + initializes test DB
vendor/bin/phpunit --testsuite local_example_testsuite
phpunit.xml is regenerated by init.php — never hand-edit. Re-run after installing a new plugin.
Test class skeleton
<?php
namespace local_example;
defined('MOODLE_INTERNAL') || die();
/**
* @group local_example
* @covers \local_example\manager
*/
final class manager_test extends \advanced_testcase {
public function test_create_item(): void {
$this->resetAfterTest();
$generator = self::getDataGenerator();
$course = $generator->create_course();
$user = $generator->create_user();
$manager = new manager();
$id = $manager->create_item($course->id, $user->id, 'hello');
global $DB;
$row = $DB->get_record('local_example_items', ['id' => $id], '*', MUST_EXIST);
$this->assertSame('hello', $row->name);
}
}
Key rules:
- File name:
<thing>_test.php, class:<thing>_test final class(Moodle policy since 4.2)@coversannotation required by Moodle CS@group <component>enables--groupfilteringvoidreturn type on test methods,: voidonsetUpself::(not$this->) for static methods likegetDataGenerator()
Data generators
Plugin generator at tests/generator/lib.php:
<?php
defined('MOODLE_INTERNAL') || die();
class local_example_generator extends component_generator_base {
public function create_item(array $record = []): \stdClass {
global $DB, $USER;
$defaults = [
'courseid' => 0,
'userid' => $USER->id,
'name' => 'Item ' . random_string(8),
'timecreated'=> time(),
];
$record = (object)array_merge($defaults, $record);
$record->id = $DB->insert_record('local_example_items', $record);
return $record;
}
}
Use:
$gen = self::getDataGenerator()->get_plugin_generator('local_example');
$item = $gen->create_item(['name' => 'test']);
Activity module generator extends testing_module_generator and implements create_instance().
Common patterns
Test an event
$sink = $this->redirectEvents();
$manager->do_thing();
$events = $sink->get_events();
$sink->close();
$this->assertCount(1, $events);
$this->assertInstanceOf(\local_example\event\thing_done::class, $events[0]);
Test an email
$sink = $this->redirectEmails();
$manager->notify($user);
$messages = $sink->get_messages();
$this->assertSame($user->email, $messages[0]->to);
Test a scheduled task
$task = new \local_example\task\cleanup();
$task->execute();
// assert side effects
Test an external (web service) function
$this->setUser($user);
$result = \local_example\external\get_items::execute($courseid);
$result = \core_external\external_api::clean_returnvalue(
\local_example\external\get_items::execute_returns(),
$result
);
$this->assertCount(2, $result);
clean_returnvalue is mandatory — catches schema mismatches.
Test an ad-hoc task
\core\task\manager::queue_adhoc_task(new \local_example\task\send_report());
$this->runAdhocTasks(\local_example\task\send_report::class);
Login as a user
$user = $this->getDataGenerator()->create_user();
$this->setUser($user); // sets $USER global
$this->setAdminUser(); // shortcut
$this->setGuestUser();
Time-travel
$this->mock_clock_with_frozen(1700000000); // Moodle 4.4+
// or in older versions, manually set timecreated/timemodified
Running tests
# Single suite
vendor/bin/phpunit --testsuite local_example_testsuite
# Single file
vendor/bin/phpunit local/example/tests/manager_test.php
# Single method
vendor/bin/phpunit --filter test_create_item local/example/tests/manager_test.php
# By group
vendor/bin/phpunit --group local_example
# Coverage (requires xdebug or pcov)
vendor/bin/phpunit --coverage-html coverage/ local/example/tests
Test database
- Separate DB defined in
config.php:$CFG->phpunit_prefix = 'phpu_'; - Reset between tests via transactions —
$this->resetAfterTest()enables it - Schema drift error: re-run
php admin/tool/phpunit/cli/init.php - "Database was modified" failure means a test mutated DB without
resetAfterTest()
Mocking
Moodle prefers integration tests with the real test DB over mocking $DB. When you must mock:
$mockDB = $this->createMock(\moodle_database::class);
$mockDB->method('get_record')->willReturn((object)['id' => 1]);
// inject via DI, never replace global
Avoid replacing the global $DB — breaks isolation.
Common Mistakes
| Mistake | Fix |
|---|---|
Forgetting $this->resetAfterTest() | Add at start of every DB-touching test |
Class not final | Add final (Moodle 4.2+ policy) |
Missing @covers | Add @covers \Fully\Qualified\Class |
Hand-editing phpunit.xml | Re-run admin/tool/phpunit/cli/init.php |
Using parent::setUp() to reset DB | Use resetAfterTest() instead |
Skipping clean_returnvalue on external fn | Always wrap external returns to catch schema bugs |
$this->getDataGenerator() (instance) | Moodle prefers self::getDataGenerator() (static) |
Asserting time with time() | Use mock_clock_with_frozen or compare with tolerance |
CI snippet (GitHub Actions)
- name: PHPUnit
run: |
php admin/tool/phpunit/cli/init.php
vendor/bin/phpunit --testsuite ${{ matrix.suite }}
References
- PHPUnit in Moodle: https://moodledev.io/general/development/tools/phpunit
- Data generators: https://moodledev.io/docs/apis/subsystems/testing/generators
- Test writing guide: https://moodledev.io/general/development/policies/testing
- Coverage: https://moodledev.io/general/development/tools/phpunit#code-coverage