From zenbu-powers
PHP IT Stage 2:紅燈生成器。對單一 .feature 的測試骨架執行完整紅燈流程: 建立 IntegrationTestCase 基類(若不存在)→ 建立 Model/Repository/Service stubs(BadMethodCallException) → 逐一實作 test method 的 Given/When/Then 邏輯(參考對應 handler skill)。 預期結果:測試執行後失敗於 BadMethodCallException(Service 方法未實作)。 可被 /aibdd.auto.php.it.control-flow 調用,也可獨立使用。
npx claudepluginhub zenbuapps/zenbu-powers --plugin zenbu-powersThis skill uses the workspace's default tool permissions.
本 skill 為 **WordPress PHP 整合測試 4-Phase TDD 流程** 中的 **Stage 2 紅燈生成器**。
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Guides code writing, review, and refactoring with Karpathy-inspired rules to avoid overcomplication, ensure simplicity, surgical changes, and verifiable success criteria.
Share bugs, ideas, or general feedback.
本 skill 為 WordPress PHP 整合測試 4-Phase TDD 流程 中的 Stage 2 紅燈生成器。
接手 Stage 1(/zenbu-powers:aibdd.auto.php.it.test-skeleton)產出的 PHPUnit 測試骨架(含 markTestIncomplete 與 Gherkin TODO 註解),完成下列三件事:
IntegrationTestCase 基類(若尚未存在)BadMethodCallException)test_* method 的 Given/When/Then 邏輯(呼叫 6 個 handler skills 的模式)產出的測試必須「紅燈」:執行 PHPUnit 時失敗於 BadMethodCallException: 尚未實作(Service 方法未實作),而非 Type error 或 Autoload error。
由 /zenbu-powers:aibdd.auto.php.it.control-flow 批次呼叫,接收 feature file 絕對路徑 作為參數:
/zenbu-powers:aibdd.auto.php.it.red specs/features/01-lesson-progress.feature
直接進入 Step 1,不詢問使用者。
使用者直接呼叫此 skill 時:
specs/features/*.feature 清單npx wp-env status → runningcomposer install 已完成,vendor/ 存在tests/integration/IntegrationTestCase.php 可正常載入vendor/yoast/wp-test-utils/ 存在測試執行後必須因 BadMethodCallException: 尚未實作 而失敗。
失敗位置應為 Service 方法被 When 呼叫時、或 Repository 方法被 Given 呼叫時。不可 為:
Error: Class App\Services\XxxService not found(autoload 設定錯誤)TypeError: ... must be of type int(型別宣告錯誤)Error: Call to undefined method(stub 方法未建立)Step 1: IntegrationTestCase 基類確認(若不存在則建立)
↓
Step 2: Model / Repository / Service Stub 建立
(方法體一律拋 BadMethodCallException)
↓
Step 3: Test Method 實作
(讀 TODO 標註 → 載入 handler → 替換 markTestIncomplete)
↓
驗證:執行 PHPUnit → 預期失敗於 BadMethodCallException
tests/integration/IntegrationTestCase.php
<?php
declare(strict_types=1);
namespace Tests\Integration;
abstract class IntegrationTestCase extends \Yoast\WPTestUtils\WPIntegration\TestCase
{
protected ?\Throwable $lastError = null;
protected mixed $queryResult = null;
protected array $ids = [];
protected object $repos;
protected object $services;
abstract protected function configure_dependencies(): void;
public function set_up(): void
{
parent::set_up();
$this->lastError = null;
$this->queryResult = null;
$this->ids = [];
$this->repos = new \stdClass();
$this->services = new \stdClass();
$this->configure_dependencies();
}
protected function assert_operation_succeeded(): void
{
$this->assertNull(
$this->lastError,
'Expected success but got: ' . ($this->lastError?->getMessage() ?? '')
);
}
protected function assert_operation_failed(): void
{
$this->assertNotNull($this->lastError, 'Expected failure but operation succeeded');
}
protected function assert_operation_failed_with_type(string $type): void
{
$this->assertNotNull($this->lastError);
$this->assertInstanceOf($type, $this->lastError);
}
protected function assert_operation_failed_with_message(string $msg): void
{
$this->assertNotNull($this->lastError);
$this->assertStringContainsString($msg, $this->lastError->getMessage());
}
}
必須包含:
$lastError, $queryResult, $ids, $repos, $servicesconfigure_dependencies()set_up() 重置狀態並呼叫 configure_dependencies()assert_operation_* helper若缺少任一項 → 補齊後繼續。
根據 Feature 中 Entity 的特性,選擇對應的 WordPress 儲存方式:
| 儲存模式 | 適用場景 | WP API | 範例 Entity |
|---|---|---|---|
| Custom Post Type | 內容型實體(有標題/內容/中繼) | register_post_type, wp_insert_post | Course, Article |
| Post Meta | 文章自訂屬性 | update_post_meta, get_post_meta | CourseMetadata |
| User Meta | 使用者相關屬性 | update_user_meta, get_user_meta | LessonProgress, UserPreference |
| Options API | 全域設定 | update_option, get_option | SiteSettings |
| Custom Taxonomy | 分類/標籤 | register_taxonomy, wp_set_object_terms | CourseCategory |
| Custom Table | 高效能/複雜查詢 | $wpdb->insert, $wpdb->get_results | Order, OrderItem, CartItem |
紅燈階段不需要實作實際 WP 操作,只需要把 Model / Repository / Service 的 class 骨架建立起來,方法體一律 throw new \BadMethodCallException('尚未實作');。
Plain PHP,使用 constructor property promotion,不依賴 WordPress:
<?php
declare(strict_types=1);
namespace App\Models;
class LessonProgress
{
public function __construct(
private int $userId,
private int $lessonId,
private int $progress,
private string $status,
) {}
public function getUserId(): int { return $this->userId; }
public function getLessonId(): int { return $this->lessonId; }
public function getProgress(): int { return $this->progress; }
public function getStatus(): string { return $this->status; }
}
僅宣告方法簽章,方法體一律拋 BadMethodCallException:
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\LessonProgress;
class LessonProgressRepository
{
public function save(LessonProgress $progress): void
{
throw new \BadMethodCallException('尚未實作');
}
public function findByUserAndLesson(int $userId, int $lessonId): ?LessonProgress
{
throw new \BadMethodCallException('尚未實作');
}
}
透過 constructor 注入 Repository,業務方法拋 BadMethodCallException:
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\LessonProgress;
use App\Repositories\LessonProgressRepository;
class LessonService
{
public function __construct(
private LessonProgressRepository $lessonProgressRepo,
) {}
public function updateVideoProgress(int $userId, int $lessonId, int $progress): void
{
throw new \BadMethodCallException('尚未實作');
}
public function getProgress(int $userId, int $lessonId): ?LessonProgress
{
throw new \BadMethodCallException('尚未實作');
}
}
src/Models/LessonProgress.php
src/Repositories/LessonProgressRepository.php
src/Services/LessonService.php
composer.json 需有對應 PSR-4 autoload 設定:
"autoload": {
"psr-4": { "App\\": "src/" }
}
若缺少,建立後執行 composer dump-autoload。
對 test-skeleton 產出的每個 test_* method:
讀取方法體內的 TODO 註解(Stage 1 產生),例如:
// TODO [Handler: aggregate-given] Given 用戶 "Alice" 在課程 1 的進度為 50%
// TODO [Handler: command] When 用戶 "Alice" 更新課程 1 的影片進度為 80%
// TODO [Handler: success-failure] Then 操作成功
// TODO [Handler: aggregate-then] And 用戶 "Alice" 在課程 1 的進度應為 80%
$this->markTestIncomplete('尚未實作');
根據 [Handler: xxx] 標註,載入對應 handler skill(透過 Skill 工具),取得該句型的 PHP 模板。
替換 $this->markTestIncomplete(...) 為 handler 定義的 PHP 程式碼。
| Gherkin 句型 | Handler Skill |
|---|---|
| Given 狀態描述(Aggregate 初始狀態) | /zenbu-powers:aibdd.auto.php.it.handlers.aggregate-given |
| Given 已完成動作 / When 寫入操作 | /zenbu-powers:aibdd.auto.php.it.handlers.command |
| When 讀取操作 | /zenbu-powers:aibdd.auto.php.it.handlers.query |
| Then DB 狀態驗證 | /zenbu-powers:aibdd.auto.php.it.handlers.aggregate-then |
| Then Response / ReadModel 驗證 | /zenbu-powers:aibdd.auto.php.it.handlers.readmodel-then |
| Then 操作成功/失敗 | /zenbu-powers:aibdd.auto.php.it.handlers.success-failure |
configure_dependencies()根據 test 中使用到的 Service / Repository,在測試類的 configure_dependencies() 建立依賴圖:
protected function configure_dependencies(): void
{
$this->repos->lessonProgress = new LessonProgressRepository();
$this->services->lesson = new LessonService($this->repos->lessonProgress);
}
public function test_成功增加影片進度(): void
{
// Given 用戶 "Alice" 在課程 1 的進度為 50%,狀態為 "進行中"
$userId = $this->factory()->user->create(['display_name' => 'Alice']);
$this->ids['Alice'] = $userId;
$this->repos->lessonProgress->save(
new LessonProgress($userId, 1, 50, 'IN_PROGRESS')
);
// ↑ 此行將拋 BadMethodCallException → 紅燈
// When 用戶 "Alice" 更新課程 1 的影片進度為 80%
try {
$this->services->lesson->updateVideoProgress($userId, 1, 80);
} catch (\Throwable $e) {
$this->lastError = $e;
}
// Then 操作成功
$this->assert_operation_succeeded();
// And 用戶 "Alice" 在課程 1 的進度應為 80%
$progress = $this->repos->lessonProgress->findByUserAndLesson($userId, 1);
$this->assertNotNull($progress);
$this->assertSame(80, $progress->getProgress());
}
完成 Step 1–3 後執行:
vendor/bin/phpunit --testsuite integration --filter={FeatureName}Test
或透過 wp-env:
npx wp-env run tests-cli --env-cwd=wp-content/plugins/{plugin} \
vendor/bin/phpunit --testsuite integration --filter={FeatureName}Test
There was 1 error:
1) Tests\Integration\LessonProgressTest::test_成功增加影片進度
BadMethodCallException: 尚未實作
/app/src/Repositories/LessonProgressRepository.php:12
/app/tests/integration/LessonProgressTest.php:28
ERRORS!
Tests: 1, Assertions: 0, Errors: 1.
✅ 這就是標準紅燈 — Value Difference 於 BadMethodCallException。
| 規則 | 說明 |
|---|---|
| R1 | 不實作業務邏輯。Service / Repository 方法體必須一律 throw new \BadMethodCallException('尚未實作'); |
| R2 | Models / Repositories / Services 置於 src/ 目錄(非 tests/)。遵循 PSR-4 autoload。 |
| R3 | 測試必須失敗(紅燈本質)。失敗原因為 BadMethodCallException,非 Type error 或 Autoload error。 |
| R4 | Stub 使用 PHP 8 constructor property promotion(public function __construct(private int $x))。 |
| R5 | 所有 src/ 與 tests/ 檔案第一行 declare(strict_types=1);。 |
| R6 | Repository 與 Service 透過 constructor DI,不使用 static / global。 |
| R7 | 中文狀態欄位(「進行中」「已完成」)以 string constant 或 Enum 處理,DB 內存英文代碼(如 IN_PROGRESS, COMPLETED)。 |
tests/integration/IntegrationTestCase.php 存在且結構完整src/Models/*.php 已建立(plain PHP + getter)src/Repositories/*.php 已建立(方法拋 BadMethodCallException)src/Services/*.php 已建立(方法拋 BadMethodCallException)configure_dependencies() 已組裝所有依賴test_* method 的 markTestIncomplete 已被 Given/When/Then 實作取代composer dump-autoload 已執行(若有新增 PSR-4 entry)BadMethodCallException: 尚未實作 錯誤 → 紅燈確認完成後告知使用者「紅燈完成,可進入 Stage 3 綠燈階段(/zenbu-powers:aibdd.auto.php.it.green)」。