|
| 1 | +--- |
| 2 | +name: phpunit-stub-test |
| 3 | +description: Adds PHPUnit test cases for this plugin using the stub pattern from tests/stubs.php and tests/bootstrap.php. Use when user says 'add test', 'write tests', 'test this function', or needs coverage for src/Plugin.php or src/whmsonic.inc.php. Adds global function stubs under namespace Detain\MyAdminWhmsonic using function_exists guards, then writes test class under Detain\MyAdminWhmsonic\Tests namespace. Do NOT use for PHPUnit mocks of MyAdmin global functions or for integration tests requiring a live WHMSonic API. |
| 4 | +--- |
| 5 | +# PHPUnit Stub Test |
| 6 | + |
| 7 | +## Critical |
| 8 | + |
| 9 | +- **Never use PHPUnit mocks for MyAdmin global functions** (`myadmin_log`, `get_service_define`, `function_requirements`, `get_module_settings`, `_`). Always add namespace-level stubs to `tests/stubs.php` instead. |
| 10 | +- All stubs must live inside `namespace Detain\MyAdminWhmsonic { }` in `tests/stubs.php` and be guarded with `function_exists('Detain\\MyAdminWhmsonic\\func_name')`. |
| 11 | +- Do NOT make live cURL calls in tests. For `src/whmsonic.inc.php` functions, test parameter signatures and source analysis via `ReflectionFunction` — not execution. |
| 12 | +- Constants `WHMSONIC_USERNAME` / `WHMSONIC_PASSWORD` must be defined in `setUpBeforeClass()` with `if (!defined(...))` guards before `require_once`-ing `src/whmsonic.inc.php`. |
| 13 | + |
| 14 | +## Instructions |
| 15 | + |
| 16 | +1. **Identify what global functions the new code calls.** Read the target source file to find any calls to `myadmin_log`, `get_service_define`, `function_requirements`, `get_module_settings`, `_`, or any `whmsonic_*` function. Verify each against existing stubs in `tests/stubs.php` before proceeding. |
| 17 | + |
| 18 | +2. **Add missing stubs to `tests/stubs.php`.** For each missing global function, append inside the `namespace Detain\MyAdminWhmsonic { }` block: |
| 19 | + ```php |
| 20 | + if (!function_exists('Detain\\MyAdminWhmsonic\\my_new_func')) { |
| 21 | + function my_new_func(...$args): void {} |
| 22 | + } |
| 23 | + ``` |
| 24 | + Return a typed value (e.g., `string`, `array`) only if the calling code uses the return value. Verify `tests/bootstrap.php` still loads `stubs.php` before `vendor/autoload.php` — do not modify bootstrap unless a new require is needed. |
| 25 | + |
| 26 | +3. **Create the test class file** under `tests/` with filename `MyFeatureTest.php`. Use this exact header: |
| 27 | + ```php |
| 28 | + <?php |
| 29 | + declare(strict_types=1); |
| 30 | + namespace Detain\MyAdminWhmsonic\Tests; |
| 31 | + use PHPUnit\Framework\TestCase; |
| 32 | + use ReflectionClass; // for Plugin tests |
| 33 | + use ReflectionFunction; // for whmsonic.inc.php function tests |
| 34 | + use Symfony\Component\EventDispatcher\GenericEvent; // for event handler tests |
| 35 | + ``` |
| 36 | + |
| 37 | +4. **For `src/Plugin.php` tests:** Use `ReflectionClass` for structural assertions. Fire event handlers by constructing `new GenericEvent($subject, $args)` where `$subject` is an anonymous class with the required methods (`getId()`, `getIp()`): |
| 38 | + ```php |
| 39 | + $serviceClass = new class { |
| 40 | + public function getId(): int { return 42; } |
| 41 | + public function getIp(): string { return '10.0.0.1'; } |
| 42 | + }; |
| 43 | + $event = new GenericEvent($serviceClass, ['category' => 'WHMSONIC_TYPE']); |
| 44 | + Plugin::getActivate($event); |
| 45 | + $this->assertTrue($event->isPropagationStopped()); |
| 46 | + ``` |
| 47 | + Assert `isPropagationStopped()` for matching category and `assertFalse` for non-matching category. |
| 48 | + |
| 49 | +5. **For `src/whmsonic.inc.php` tests:** Define constants and `require_once` the file in `setUpBeforeClass()`, then use `ReflectionFunction` for parameter count/name assertions and `file_get_contents(dirname(__DIR__).'/src/whmsonic.inc.php')` + `assertStringContainsString` for API command verification. Do not call the actual functions. |
| 50 | + |
| 51 | +6. **Run tests:** `vendor/bin/phpunit` from the package root. All tests must pass before considering work complete. |
| 52 | + |
| 53 | +## Examples |
| 54 | + |
| 55 | +**User says:** "Add a test for the new `whmsonic_reboot` function in `src/whmsonic.inc.php`." |
| 56 | + |
| 57 | +**Actions taken:** |
| 58 | +1. Read `src/whmsonic.inc.php` — find `whmsonic_reboot($licenseip)` calls `myadmin_log` and `curl_*`. |
| 59 | +2. `myadmin_log` stub already exists in `tests/stubs.php` — no change needed. |
| 60 | +3. Create `tests/WhmsonicRebootTest.php`: |
| 61 | + ```php |
| 62 | + <?php |
| 63 | + declare(strict_types=1); |
| 64 | + namespace Detain\MyAdminWhmsonic\Tests; |
| 65 | + use PHPUnit\Framework\TestCase; |
| 66 | + use ReflectionFunction; |
| 67 | + class WhmsonicRebootTest extends TestCase { |
| 68 | + public static function setUpBeforeClass(): void { |
| 69 | + if (!defined('WHMSONIC_USERNAME')) define('WHMSONIC_USERNAME', 'test_user'); |
| 70 | + if (!defined('WHMSONIC_PASSWORD')) define('WHMSONIC_PASSWORD', 'test_pass'); |
| 71 | + require_once dirname(__DIR__) . '/src/whmsonic.inc.php'; |
| 72 | + } |
| 73 | + public function testWhmsonicRebootExists(): void { |
| 74 | + $this->assertTrue(function_exists('whmsonic_reboot')); |
| 75 | + } |
| 76 | + public function testWhmsonicRebootParameterName(): void { |
| 77 | + $ref = new ReflectionFunction('whmsonic_reboot'); |
| 78 | + $this->assertSame('licenseip', $ref->getParameters()[0]->getName()); |
| 79 | + } |
| 80 | + public function testWhmsonicRebootUsesRebootCommand(): void { |
| 81 | + $ref = new ReflectionFunction('whmsonic_reboot'); |
| 82 | + $lines = array_slice(explode("\n", file_get_contents($ref->getFileName())), |
| 83 | + $ref->getStartLine() - 1, $ref->getEndLine() - $ref->getStartLine() + 1); |
| 84 | + $this->assertStringContainsString('cmd=reboot', implode("\n", $lines)); |
| 85 | + } |
| 86 | + } |
| 87 | + ``` |
| 88 | +4. Run `vendor/bin/phpunit` — all pass. |
| 89 | + |
| 90 | +**Result:** New test class follows exact patterns from `WhmsonicFunctionsTest.php`, no mocks used. |
| 91 | + |
| 92 | +## Common Issues |
| 93 | + |
| 94 | +- **"Call to undefined function Detain\\MyAdminWhmsonic\\myadmin_log"**: The stub is missing or in the wrong namespace block. Open `tests/stubs.php` and confirm the function is declared inside `namespace Detain\MyAdminWhmsonic { }`, not global namespace. |
| 95 | +- **"Cannot redeclare function"**: You called `require_once src/whmsonic.inc.php` more than once across test classes in the same process. Add `if (function_exists('activate_whmsonic')) return;` at the top of the include, or use `require_once` consistently — never `require`. |
| 96 | +- **"Constant WHMSONIC_USERNAME already defined"**: Always guard with `if (!defined('WHMSONIC_USERNAME'))` in `setUpBeforeClass()`. |
| 97 | +- **Tests pass locally but fail in CI (function not found)**: `tests/bootstrap.php` must load `stubs.php` before `vendor/autoload.php`. Confirm line order: `require_once __DIR__ . '/stubs.php';` then `require_once dirname(__DIR__) . '/vendor/autoload.php';`. |
| 98 | +- **`$event->isPropagationStopped()` returns false unexpectedly**: The anonymous `$subject` class is missing a method called by the handler (e.g., `getId()`). Read `src/Plugin.php` to check what methods the handler calls on `$event->getSubject()`. |
0 commit comments