plugin-test
Writes PHPUnit tests in `tests/` using the stub pattern from `tests/bootstrap.php`. Use when user says 'add test', 'test this function', 'write phpunit', 'cover this', or modifies `src/Plugin.php` or
SKILL.md.claude/skills/plugin-test/SKILL.md
GitHub<!-- myadmin-contract-harness-notice -->
⚠️ Read this before the rest of the file
This package is on the shared plugin contract harness. Parts of the guidance below predate it and are now wrong in one specific way:
Any instruction here that a plugin's
getHooks()/getSettings()/getActivate()/getDeactivate()/getQueue()must not be called — that only its existence, visibility or parameter count may be checked throughReflectionClass— no longer applies. That rule existed because those methods reference bare constants (PRORATE_BILLINGand friends) that only a live MyAdmin request defines, so calling them from a test used to fatal. The harness defines them first. It then executes the handlers for real, in a process of its own.A reflection-only assertion passes whether or not the thing works:
getActivate()can exist, be public, be static, take one argument, and still fatal the moment it runs. Three real production bugs in this fleet were sitting behind assertions of exactly that shape.Use the
plugin-contract-testsskill for anything touchingtests/ContractTest.php, the contract inspectors, orcomposer myadmin:scaffold-tests.Everything else in this file is still accurate and still applies — this package's own classes, its API wrappers, its fixtures, its bootstrap, and the reasons certain classes must not be constructed. Nothing below has been removed.
plugin-test
Critical
- Never call live KA API — all tests must run without network access. Use Reflection to inspect signatures; do not instantiate
\Detain\Parallels\Parallelsin tests. - Always guard new global stubs with
if (!function_exists(...))intests/bootstrap.php— the bootstrap runs once for the whole suite. - Test files go in
tests/, namespaceDetain\MyAdminParallels\Tests, extendPHPUnit\Framework\TestCase, usedeclare(strict_types=1). - Run
composer testto verify before considering a task done.
Instructions
Identify what needs covering. For
src/Plugin.php→ create or updatetests/PluginTest.php. Forsrc/parallels.inc.php→ create or updatetests/ParallelsIncTest.php.Check
tests/bootstrap.phpfor missing stubs. Opensrc/file being tested and list every bare function call that is a MyAdmin global (myadmin_log,get_service_define,function_requirements,request_log,get_module_settings, etc.). For each one not already stubbed, add totests/bootstrap.php:if (!function_exists('new_global_fn')) { function new_global_fn(): void {} }Verify stub was added before writing tests that depend on it.
Write Plugin tests (
tests/PluginTest.php). UseReflectionClass— never mockPlugin. Required test groups:- Static properties (
$name,$description,$module,$type,$help) — assert type string and expected values. getHooks()returns array with keys prefixed byPlugin::$module . '.'plus'function.requirements'.- Each hook value is
[Plugin::class, 'methodName'](2-element array). - Each referenced method exists, is
publicandstatic, accepts exactly oneGenericEventparameter. - Handler aliases are identical:
licenses.activate === licenses.reactivate,licenses.deactivate === licenses.deactivate_ip.
use Detain\MyAdminParallels\Plugin; use PHPUnit\Framework\TestCase; use ReflectionClass; use Symfony\Component\EventDispatcher\GenericEvent; class PluginTest extends TestCase { public function testModuleProperty(): void { $this->assertSame('licenses', Plugin::$module); } public function testHookMethodsArePublicAndStatic(): void { $hooks = Plugin::getHooks(); $rc = new ReflectionClass(Plugin::class); foreach ($hooks as [$class, $method]) { $m = $rc->getMethod($method); $this->assertTrue($m->isPublic()); $this->assertTrue($m->isStatic()); } } }- Static properties (
Write inc function tests (
tests/ParallelsIncTest.php). Load the file once insetUpBeforeClass(), then useReflectionFunctionfor all assertions — do not call the functions.public static function setUpBeforeClass(): void { if (!function_exists('activate_parallels')) { require_once dirname(__DIR__) . '/src/parallels.inc.php'; } } public function testActivateParallelsSignature(): void { $ref = new \ReflectionFunction('activate_parallels'); $params = $ref->getParameters(); $this->assertCount(3, $params); $this->assertSame('ipAddress', $params[0]->getName()); $this->assertSame('type', $params[1]->getName()); $this->assertSame('addons', $params[2]->getName()); $this->assertTrue($params[2]->isDefaultValueAvailable()); $this->assertSame('', $params[2]->getDefaultValue()); $this->assertSame(2, $ref->getNumberOfRequiredParameters()); }Standard assertions for every new inc function:
function_exists, parameter count, parameter names, required parameter count,hasReturnType() === false(inc functions are untyped legacy style).Run and verify:
composer testAll tests must pass with no errors or warnings before finishing.
Examples
User says: "Add tests for a new reactivate_parallels($ipAddress, $key) function I just added to src/parallels.inc.php"
Actions taken:
- Check
tests/bootstrap.php—myadmin_logandrequest_logalready stubbed. No new globals needed. - In
tests/ParallelsIncTest.php, add insideParallelsIncTest:public function testReactivateParallelsFunctionExists(): void { $this->assertTrue(function_exists('reactivate_parallels')); } public function testReactivateParallelsSignature(): void { $ref = new \ReflectionFunction('reactivate_parallels'); $params = $ref->getParameters(); $this->assertCount(2, $params); $this->assertSame('ipAddress', $params[0]->getName()); $this->assertSame('key', $params[1]->getName()); $this->assertSame(2, $ref->getNumberOfRequiredParameters()); $this->assertFalse($ref->hasReturnType()); } - Run
composer test— passes.
Result: Two new tests in tests/ParallelsIncTest.php verifying existence and signature without touching the KA API.
Common Issues
- "Call to undefined function myadmin_log()" — the src file calls a global not stubbed yet. Add
if (!function_exists('myadmin_log')) { function myadmin_log(): void {} }totests/bootstrap.php. - "Cannot find autoloader" — bootstrap tries two paths (
__DIR__.'/../vendor/autoload.php'then__DIR__.'/../../../../vendor/autoload.php'). Runcomposer installfrom the plugin root sovendor/autoload.phpexists. - "Class Symfony\Component\EventDispatcher\GenericEvent not found" — run
composer install; it's pulled in viasymfony/event-dispatcher. - ReflectionFunction throws "Function activate_parallels() does not exist" — the
setUpBeforeClassguardif (!function_exists(...))skipped therequire_once. Either the function was never added to the file, or a typo in the function name. Grep:grep -n 'function activate_parallels' src/parallels.inc.php. - Tests pass locally but fail in CI — CI may not have the inner
vendor/path. Confirmphpunit.xml.distbootstrapattribute points totests/bootstrap.phpand thatcomposer installruns before the test step.