Install
$ agentstack add skill-saadrahman01-claude-moodle-dev-moodle-phpunit-testing ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Moodle PHPUnit Testing
Overview
Moodle ships its own PHPUnit harness with test bootstrap, transactional resets, and data generators. Tests live in /tests/_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
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:
_test.php, class:_test final class(Moodle policy since 4.2)@coversannotation required by Moodle CS@groupenables--groupfilteringvoidreturn type on test methods,: voidonsetUpself::(not$this->) for static methods likegetDataGenerator()
Data generators
Plugin generator at tests/generator/lib.php:
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
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: SaadRahman01
- Source: SaadRahman01/claude-moodle-dev
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.