Skip to content
generate-acceptance-test logo

Generate Acceptance Test Skill

generate-acceptance-test

從規格目錄的 acceptance.yaml 生成/維護 BDD/ezSpec 測試。使用類似 Gherkin 語法,AI 自動產生 step definition(開發人員不需要手寫),驗收測試規格即為 Executable Specification。

SKILL.md

Full skill instructions

Generate Acceptance Test Skill

觸發時機

  • analyze-frame 產生規格目錄後
  • acceptance.yaml 更新時
  • 需求異動需同步更新驗收測試時
  • 代碼生成前,希望先鎖定可執行的驗收測試

核心任務

  1. 解析 acceptance.yaml 的測試規格
  2. 自動生成 ezSpec step definition(開發人員不需要手寫)
  3. 維護規格與測試的一致性
  4. 建立與 Frame Concerns 的可追溯連結

工具腳本 (scripts/)

generate_tests.py - 測試生成器

從 acceptance.yaml 生成各語言的 BDD 測試骨架。支援新格式 (acceptance_criteria) 和舊格式 (scenarios)。

使用方式:

# 生成 Gherkin .feature 檔案
python ~/​.claude/​skills/​generate-acceptance-test/​scripts/​generate_tests.py \
    docs/​specs/​create-workflow/ --lang gherkin

# 生成 TypeScript Cucumber.js step definitions
python ~/​.claude/​skills/​generate-acceptance-test/​scripts/​generate_tests.py \
    docs/​specs/​create-workflow/ --lang typescript --output tests/​acceptance/

# 生成 Go Ginkgo 測試
python ~/​.claude/​skills/​generate-acceptance-test/​scripts/​generate_tests.py \
    docs/​specs/​create-workflow/ --lang go --output tests/​acceptance/

# 生成 Rust cucumber-rs 測試
python ~/​.claude/​skills/​generate-acceptance-test/​scripts/​generate_tests.py \
    docs/​specs/​create-workflow/ --lang rust --output tests/​acceptance/

支援語言:

LanguageFlagOutput
Gherkin--lang gherkin{feature}.feature
TypeScript--lang typescript{feature}.steps.ts
Go--lang go{feature}_test.go
Rust--lang rust{feature}.rs

格式相容性:

腳本自動偵測並支援兩種格式:

# 新格式 (推薦)
acceptance_criteria:
  - id: AC1
    trace:
      requirement: [CBF-REQ-1]
      frame_concerns: [FC1]
    given: ["..."]
    when: ["..."]
    then: ["..."]

# 舊格式 (向下相容)
acceptance:
  scenarios:
    - id: AT1
      given:
        - condition: "..."
      when:
        - action: "..."
      then:
        - expectation: "..."

關鍵概念

Executable Specification

  • 驗收測試規格 = 可執行的規格
  • 使用類似 Gherkin 的 given/​when/​then 語法
  • AI 自動生成 step definition,開發人員不需要手寫
  • 規格變更時,測試自動同步

目錄結構

docs/​specs/​{feature-name}/
├── frame.yaml
├── acceptance.yaml            # 測試規格 (輸入) - 在根目錄
├── requirements/
│   └── cbf-req-1-{feature}.yaml
├── machine/
│   ├── controller.yaml
│   ├── machine.yaml
│   └── use-case.yaml
├── controlled-domain/
│   └── aggregate.yaml
└── ...

acceptance.yaml 格式

# docs/​specs/​{feature-name}/​acceptance.yaml
# 注意:放在規格根目錄,不是 acceptance/ 子目錄

acceptance_criteria:

  # ---------------------------------------------------------------------------
  # Happy Path - 成功場景
  # ---------------------------------------------------------------------------
  
  - id: AC1
    type: business          # business | technical | edge-case
    test_tier: usecase      # usecase | integration | e2e
    name: "Create a valid workflow (board existence is not synchronously validated)"
    
    # 追溯連結
    trace:
      requirement:
        - CBF-REQ-1
      frame_concerns:
        - WF-FC-AUTH        # Authorization
        - FC2               # Observability & Auditability
    
    # 連結到生成的測試
    tests_anchor:
      - tests#success
      - tests#event-published
    
    # Given-When-Then 規格
    given:
      - "A boardId <boardId> is provided (existence is NOT synchronously validated in this bounded context)"
      - "A user <userId> is authorized to create workflows for that boardId"
    
    when:
      - "The user requests to create a workflow with boardId <boardId> and name <workflowName>"
    
    then:
      - "The request succeeds"
      - "A Workflow is created and belongs to Board <boardId>"
      - "The Workflow has name <workflowName>"
      - "The Workflow is active (not deleted)"
      - "The Workflow starts in an empty structure state (no stages/​lanes configured yet)"
    
    and:
      - "A WorkflowCreated domain event is published for downstream consumers"
    
    # 測試資料範例
    examples:
      - boardId: "board-001"
        userId: "user-123"
        workflowName: "First workflow"

  # ---------------------------------------------------------------------------
  # Error Cases
  # ---------------------------------------------------------------------------
  
  - id: AC2
    type: business
    test_tier: usecase
    name: "Reject workflow creation when not authorized"
    
    trace:
      requirement:
        - CBF-REQ-1
      frame_concerns:
        - WF-FC-AUTH
    
    tests_anchor:
      - tests#unauthorized
    
    given:
      - "A boardId <boardId> is provided"
      - "A user <userId> is NOT authorized to create workflows for that boardId"
    
    when:
      - "The user requests to create a workflow with boardId <boardId>"
    
    then:
      - "The request fails with AuthorizationError"
      - "No Workflow is created"
      - "No domain event is published"
    
    examples:
      - boardId: "board-001"
        userId: "unauthorized-user"

ezSpec Java 生成(Fluent API)

AI 自動生成的 ezSpec 測試,開發人員不需要手寫 step definition:

// tests/​acceptance/​CreateWorkflowAcceptanceTest.java
// Auto-generated from acceptance.yaml

/​**
 * AC1: Create a valid workflow successfully
 * Validates:
 * - then: workflow is created with correct boardId
 * - then: workflow is not deleted (isDeleted = false)
 * - then: workflow has no lanes/​stages initially
 * - then: WorkflowCreated event is published
 */
@EzScenario(rule = SUCCESSFUL_CREATION_RULE)
public void should_create_workflow_successfully() {
    feature.newScenario()
        .Given("a user wants to create a workflow for a board", ScenarioEnvironment env -> {
            String workflowId = UUID.randomUUID().toString();
            String boardId = UUID.randomUUID().toString();
            String userId = "test-user";

            env.put("workflowId", workflowId)
               .put("boardId", boardId)
               .put("userId", userId)
               .put("name", "Development Workflow");
        })
        .When("the workflow is created", ScenarioEnvironment env -> {...})
        .ThenSuccess(ScenarioEnvironment env -> {...})
        .And("the workflow should be persisted with correct boardId", ScenarioEnvironment env -> {...})
        .And("the workflow should not be deleted", ScenarioEnvironment env -> {...})
        .And("the workflow should have no root stages initially", ScenarioEnvironment env -> {...})
        .And("a WorkflowCreated event should be published", ScenarioEnvironment env -> {
            await().atMost(timeout: 5, TimeUnit.SECONDS).untilAsserted(() -> {
                assertThat(notifyFakeHandleAllEventsService.getHandledEventsSize()).isEqualTo(expected: 1);
                assertThat(notifyFakeHandleAllEventsService.handledEventTimes(WorkflowEvents.WorkflowCreated.class)).isEqualTo(1);
            });

            WorkflowEvents.WorkflowCreated event = (WorkflowEvents.WorkflowCreated) notifyFakeHandleAllEventsService.getEvent(0);
            var input = env.get("input", CreateWorkflowInput.class);
            assertThat(event.workflowId().value()).isEqualTo(input.workflowId);
            assertThat(event.boardId().value()).isEqualTo(input.boardId);
            assertThat(event.name()).isEqualTo(input.name);
        })
        .Execute();
}

ezSpec 生成規則

  1. 方法命名:should_{action}_{outcome} 格式

  2. JavaDoc 註解:包含 AC ID 和 Validates 項目

  3. Fluent API:.Given() → .When() → .ThenSuccess() / .ThenFailure() → .And() → .Execute()

  4. Lambda 環境:使用 ScenarioEnvironment 傳遞狀態

  5. 事件驗證:使用 await().atMost() 處理非同步事件 boardId: "board-123" name: "Sprint 1" operatorId: "user-456"

    • name: "invalidWorkflowInput" type: "CreateWorkflowInput" value: boardId: "" name: "" operatorId: "user-456"

---

## Error Case ezSpec 範例

```java
/​**
 * AC2: Reject workflow creation when not authorized
 * Validates:
 * - frame_concerns: WF-FC-AUTH
 */
@EzScenario(rule = AUTHORIZATION_RULE)
public void should_reject_when_not_authorized() {
    feature.newScenario()
        .Given("a user is not authorized to create workflows", ScenarioEnvironment env -> {
            String boardId = UUID.randomUUID().toString();
            String userId = "unauthorized-user";
            
            // Mock authorization service to deny
            when(authorizationService.hasCapability(userId, "create_workflow", boardId))
                .thenReturn(false);
            
            env.put("boardId", boardId)
               .put("userId", userId);
        })
        .When("the user attempts to create a workflow", ScenarioEnvironment env -> {
            var input = CreateWorkflowInput.builder()
                .boardId(env.get("boardId", String.class))
                .name("Test Workflow")
                .operatorId(env.get("userId", String.class))
                .build();
            env.put("input", input);
            
            try {
                useCase.execute(input);
                env.put("error", null);
            } catch (Exception e) {
                env.put("error", e);
            }
        })
        .ThenFailure(AuthorizationException.class, ScenarioEnvironment env -> {
            var error = env.get("error", Exception.class);
            assertThat(error).isInstanceOf(AuthorizationException.class);
        })
        .And("no workflow should be created", ScenarioEnvironment env -> {
            var input = env.get("input", CreateWorkflowInput.class);
            var workflow = workflowRepository.findById(WorkflowId.of(input.workflowId));
            assertThat(workflow).isEmpty();
        })
        .And("no domain event should be published", ScenarioEnvironment env -> {
            assertThat(notifyFakeHandleAllEventsService.getHandledEventsSize()).isEqualTo(0);
        })
        .Execute();
}

生成的 Gherkin .feature 檔案

# docs/​specs/​create-workflow/​generated/​create-workflow.feature
# Auto-generated from acceptance.yaml - DO NOT EDIT DIRECTLY
# Last generated: {ISO-8601}
# Validates: WF-FC-AUTH, FC2

@feature-create-workflow
Feature: Create Workflow
  As a board member
  I want to create a workflow for my board
  So that I can organize my work into stages and lanes

  # ===== Happy Path =====
  
  @smoke @api @AC1
  Scenario Outline: Create a valid workflow successfully
    # Trace: CBF-REQ-1
    # Frame Concerns: WF-FC-AUTH, FC2
    Given A boardId <boardId> is provided (existence is NOT synchronously validated in this bounded context)
    And A user <userId> is authorized to create workflows for that boardId
    When The user requests to create a workflow with boardId <boardId> and name <workflowName>
    Then The request succeeds
    And A Workflow is created and belongs to Board <boardId>
    And The Workflow has name <workflowName>
    And The Workflow is active (not deleted)
    And The Workflow starts in an empty structure state (no stages/​lanes configured yet)
    And A WorkflowCreated domain event is published for downstream consumers

    Examples:
      | boardId   | userId   | workflowName   |
      | board-001 | user-123 | First workflow |

  # ===== Error Cases =====
  
  @security @AC2
  Scenario Outline: Reject workflow creation when not authorized
    # Trace: CBF-REQ-1
    # Frame Concerns: WF-FC-AUTH
    Given A boardId <boardId> is provided
    And A user <userId> is NOT authorized to create workflows for that boardId
    When The user requests to create a workflow with boardId <boardId>
    Then The request fails with AuthorizationError
    And No Workflow is created
    And No domain event is published

    Examples:
      | boardId   | userId            |
      | board-001 | unauthorized-user |

TypeScript 測試骨架生成

// tests/​acceptance/​CreateWorkflow.spec.ts
// Auto-generated from acceptance.yaml

import { describe, it, beforeEach, expect } from 'vitest';
import { CreateWorkflowUseCase } from '@/​application/​use-cases/​CreateWorkflowUseCase';
import { InMemoryWorkflowRepository } from '@/​infrastructure/​repositories/​InMemoryWorkflowRepository';
import { MockEventPublisher } from '@/​tests/​mocks/​MockEventPublisher';
import { AuthFixture, BoardFixture } from '@/​tests/​fixtures';

describe('Feature: Create Workflow', () => {
  let useCase: CreateWorkflowUseCase;
  let workflowRepository: InMemoryWorkflowRepository;
  let eventPublisher: MockEventPublisher;

  beforeEach(() => {
    workflowRepository = new InMemoryWorkflowRepository();
    eventPublisher = new MockEventPublisher();
    useCase = new CreateWorkflowUseCase(
      /​* dependencies injected */
    );
  });

  // ===== AT1: Successfully create workflow =====
  // Validates: POST1, INV1
  // Tags: @smoke @api
  describe('Scenario: Successfully create workflow', () => {
    it('should create workflow with generated ID', async () => {
      // Given
      const user = AuthFixture.authenticatedUser();
      const board = await BoardFixture.exists('board-123');
      await BoardFixture.memberOf(user.id, board.id);

      // When
      const input = {
        boardId: 'board-123',
        name: 'Sprint 1',
        operatorId: user.id,
      };
      const result = await useCase.execute(input);

      // Then
      expect(result.workflowId).toBeDefined();
      expect(result.workflowId).not.toBeNull();
    });

    it('should publish WorkflowCreated event', async () => {
      // Given
      const user = AuthFixture.authenticatedUser();
      await BoardFixture.memberOf(user.id, 'board-123');

      // When
      await useCase.execute({
        boardId: 'board-123',
        name: 'Sprint 1',
        operatorId: user.id,
      });

      // Then
      expect(eventPublisher.published).toContainEqual(
        expect.objectContaining({ type: 'WorkflowCreatedEvent' })
      );
    });
  });

  // ===== AT2: Fail when user is not authorized =====
  // Validates: XC1 (Authorization)
  // Tags: @security
  describe('Scenario: Fail when user is not authorized', () => {
    it('should throw UnauthorizedError', async () => {
      // Given
      const user = AuthFixture.authenticatedUser();
      // User is NOT a board member

      // When & Then
      await expect(
        useCase.execute({
          boardId: 'board-123',
          name: 'Sprint 1',
          operatorId: user.id,
        })
      ).rejects.toThrow(UnauthorizedError);
    });

    it('should not create any workflow', async () => {
      // Given
      const user = AuthFixture.authenticatedUser();
      const initialCount = await workflowRepository.count();

      // When
      try {
        await useCase.execute({
          boardId: 'board-123',
          name: 'Sprint 1',
          operatorId: user.id,
        });
      } catch (e) {
        // Expected
      }

      // Then
      expect(await workflowRepository.count()).toBe(initialCount);
    });
  });

  // ===== AT3: Handle concurrent workflow creation =====
  // Validates: FC2 (Concurrency)
  // Tags: @concurrency
  describe('Scenario: Handle concurrent workflow creation', () => {
    it('should only create one workflow', async () => {
      // Given
      const user1 = AuthFixture.authenticatedUser('user-1');
      const user2 = AuthFixture.authenticatedUser('user-2');
      await BoardFixture.memberOf(user1.id, 'board-123');
      await BoardFixture.memberOf(user2.id, 'board-123');

      // When: Concurrent execution
      const [result1, result2] = await Promise.allSettled([
        useCase.execute({ boardId: 'board-123', name: 'Sprint 1', operatorId: user1.id }),
        useCase.execute({ boardId: 'board-123', name: 'Sprint 1', operatorId: user2.id }),
      ]);

      // Then
      const fulfilled = [result1, result2].filter(r => r.status === 'fulfilled');
      const rejected = [result1, result2].filter(r => r.status === 'rejected');
      
      expect(fulfilled.length).toBe(1);
      expect(rejected.length).toBe(1);
      expect(rejected[0].reason).toBeInstanceOf(ConflictError);
    });
  });
});

Go 測試骨架生成

// tests/​acceptance/​create_workflow_test.go
// Auto-generated from acceptance.yaml

package acceptance

import (
    "context"
    "testing"
    "sync"

    "github.com/​stretchr/​testify/​assert"
    "github.com/​stretchr/​testify/​require"
    
    "myapp/​application/​usecase"
    "myapp/​tests/​fixtures"
    "myapp/​tests/​mocks"
)

func TestCreateWorkflow(t *testing.T) {
    // ===== AT1: Successfully create workflow =====
    t.Run("Scenario: Successfully create workflow", func(t *testing.T) {
        t.Run("should create workflow with generated ID", func(t *testing.T) {
            // Given
            user := fixtures.AuthenticatedUser(t)
            board := fixtures.BoardExists(t, "board-123")
            fixtures.MemberOf(t, user.ID, board.ID)
            
            repo := mocks.NewInMemoryWorkflowRepository()
            eventPub := mocks.NewMockEventPublisher()
            uc := usecase.NewCreateWorkflowUseCase(repo, eventPub)

            // When
            input := usecase.CreateWorkflowInput{
                BoardID:    "board-123",
                Name:       "Sprint 1",
                OperatorID: user.ID,
            }
            result, err := uc.Execute(context.Background(), input)

            // Then
            require.NoError(t, err)
            assert.NotEmpty(t, result.WorkflowID)
        })

        t.Run("should publish WorkflowCreated event", func(t *testing.T) {
            // Given
            user := fixtures.AuthenticatedUser(t)
            fixtures.MemberOf(t, user.ID, "board-123")
            
            repo := mocks.NewInMemoryWorkflowRepository()
            eventPub := mocks.NewMockEventPublisher()
            uc := usecase.NewCreateWorkflowUseCase(repo, eventPub)

            // When
            _, err := uc.Execute(context.Background(), usecase.CreateWorkflowInput{
                BoardID:    "board-123",
                Name:       "Sprint 1",
                OperatorID: user.ID,
            })

            // Then
            require.NoError(t, err)
            assert.Contains(t, eventPub.Published(), "WorkflowCreatedEvent")
        })
    })

    // ===== AT2: Fail when user is not authorized =====
    t.Run("Scenario: Fail when user is not authorized", func(t *testing.T) {
        t.Run("should return UnauthorizedError", func(t *testing.T) {
            // Given
            user := fixtures.AuthenticatedUser(t)
            // User is NOT a board member

            repo := mocks.NewInMemoryWorkflowRepository()
            eventPub := mocks.NewMockEventPublisher()
            authSvc := mocks.NewDenyAllAuthorizationService()
            uc := usecase.NewCreateWorkflowUseCase(repo, eventPub, authSvc)

            // When
            _, err := uc.Execute(context.Background(), usecase.CreateWorkflowInput{
                BoardID:    "board-123",
                Name:       "Sprint 1",
                OperatorID: user.ID,
            })

            // Then
            assert.ErrorIs(t, err, domain.ErrUnauthorized)
        })
    })

    // ===== AT3: Handle concurrent workflow creation =====
    t.Run("Scenario: Handle concurrent workflow creation", func(t *testing.T) {
        t.Run("should only create one workflow", func(t *testing.T) {
            // Given
            user1 := fixtures.AuthenticatedUser(t)
            user2 := fixtures.AuthenticatedUser(t)
            fixtures.MemberOf(t, user1.ID, "board-123")
            fixtures.MemberOf(t, user2.ID, "board-123")

            repo := mocks.NewInMemoryWorkflowRepository()
            eventPub := mocks.NewMockEventPublisher()
            uc := usecase.NewCreateWorkflowUseCase(repo, eventPub)

            // When: Concurrent execution
            var wg sync.WaitGroup
            results := make(chan error, 2)

            for _, userID := range []string{user1.ID, user2.ID} {
                wg.Add(1)
                go func(uid string) {
                    defer wg.Done()
                    _, err := uc.Execute(context.Background(), usecase.CreateWorkflowInput{
                        BoardID:    "board-123",
                        Name:       "Sprint 1",
                        OperatorID: uid,
                    })
                    results <- err
                }(userID)
            }
            wg.Wait()
            close(results)

            // Then
            var successCount, conflictCount int
            for err := range results {
                if err == nil {
                    successCount++
                } else if errors.Is(err, domain.ErrConflict) {
                    conflictCount++
                }
            }
            
            assert.Equal(t, 1, successCount)
            assert.Equal(t, 1, conflictCount)
        })
    })
}

同步檢查機制

當規格更新時,Skill 會:

  1. 偵測變更:比對 acceptance.yaml 的變更
  2. 標記過時測試:在 .feature 檔案中標記需更新的場景
  3. 生成差異報告:列出需要同步的項目
# 同步狀態報告
sync_report:
  generated_at: "2024-12-25T10:00:00Z"
  
  in_sync:
    - AT1
    - AT2
  
  out_of_sync:
    - id: AT3
      reason: "acceptance.yaml updated, feature file not regenerated"
      diff: "then clause changed"
  
  missing:
    - id: AT4
      reason: "New scenario added to acceptance.yaml"

品質檢查清單

  • 每個 scenario 是否都有可執行的 Given/​When/​Then?
  • 是否涵蓋 happy-path、error-case、edge-case?
  • 是否與 Frame Concerns 建立 validates_concerns 連結?
  • 是否與 contracts 建立 validates_contracts 連結?
  • 測試名稱、檔名是否對應 feature-name?
  • 併發場景是否有測試 (若 FC 包含 Concurrency)?

BDD 框架支援

本 Skill 支援以下語言與 BDD 框架:

語言框架特點
JavaezSpecFluent API, 無需 step definition
GoGinkgo + GomegaBDD 風格, 表格驅動測試
TypeScriptCucumber.js / Jest-CucumberGherkin 原生支援
Rustcucumber-rsAsync 支援, 宏輔助

Go: Ginkgo + Gomega 生成

// tests/​acceptance/​create_workflow_test.go
// Auto-generated from acceptance.yaml
// Framework: Ginkgo v2 + Gomega

package acceptance_test

import (
    "context"
    "testing"

    . "github.com/​onsi/​ginkgo/​v2"
    . "github.com/​onsi/​gomega"

    "myapp/​application/​usecase"
    "myapp/​domain"
    "myapp/​tests/​fixtures"
    "myapp/​tests/​mocks"
)

func TestCreateWorkflow(t *testing.T) {
    RegisterFailHandler(Fail)
    RunSpecs(t, "Create Workflow Suite")
}

var _ = Describe("Feature: Create Workflow", func() {
    var (
        repo     *mocks.InMemoryWorkflowRepository
        eventPub *mocks.MockEventPublisher
        authSvc  *mocks.MockAuthorizationService
        uc       *usecase.CreateWorkflowUseCase
    )

    BeforeEach(func() {
        repo = mocks.NewInMemoryWorkflowRepository()
        eventPub = mocks.NewMockEventPublisher()
        authSvc = mocks.NewMockAuthorizationService()
        uc = usecase.NewCreateWorkflowUseCase(repo, eventPub, authSvc)
    })

    // ===== AC1: Create a valid workflow successfully =====
    // Trace: CBF-REQ-1
    // Frame Concerns: WF-FC-AUTH, FC2
    Describe("Scenario: Create a valid workflow successfully", Label("smoke", "api", "AC1"), func() {
        var (
            ctx    context.Context
            input  usecase.CreateWorkflowInput
            result *usecase.CreateWorkflowOutput
            err    error
        )

        BeforeEach(func() {
            ctx = context.Background()
        })

        When("a user is authorized and requests to create a workflow", func() {
            BeforeEach(func() {
                // Given
                user := fixtures.AuthenticatedUser()
                authSvc.AllowCapability(user.ID, "create_workflow", "board-001")

                // When
                input = usecase.CreateWorkflowInput{
                    BoardID:    "board-001",
                    Name:       "First workflow",
                    OperatorID: user.ID,
                }
                result, err = uc.Execute(ctx, input)
            })

            It("should succeed", func() {
                Expect(err).NotTo(HaveOccurred())
            })

            It("should create a workflow with the correct boardId", func() {
                Expect(result.WorkflowID).NotTo(BeEmpty())
                
                workflow, err := repo.FindByID(ctx, result.WorkflowID)
                Expect(err).NotTo(HaveOccurred())
                Expect(workflow.BoardID).To(Equal("board-001"))
            })

            It("should have the correct name", func() {
                workflow, _ := repo.FindByID(ctx, result.WorkflowID)
                Expect(workflow.Name).To(Equal("First workflow"))
            })

            It("should be active (not deleted)", func() {
                workflow, _ := repo.FindByID(ctx, result.WorkflowID)
                Expect(workflow.IsDeleted).To(BeFalse())
            })

            It("should start with empty structure", func() {
                workflow, _ := repo.FindByID(ctx, result.WorkflowID)
                Expect(workflow.Stages).To(BeEmpty())
                Expect(workflow.Lanes).To(BeEmpty())
            })

            It("should publish WorkflowCreated event", func() {
                Eventually(func() int {
                    return eventPub.EventCount()
                }).Should(Equal(1))

                event := eventPub.LastEvent()
                Expect(event).To(BeAssignableToTypeOf(&domain.WorkflowCreatedEvent{}))
            })
        })
    })

    // ===== AC2: Reject when not authorized =====
    // Frame Concerns: WF-FC-AUTH
    Describe("Scenario: Reject workflow creation when not authorized", Label("security", "AC2"), func() {
        When("a user is NOT authorized", func() {
            var err error

            BeforeEach(func() {
                // Given: user is not authorized
                authSvc.DenyAll()

                // When
                input := usecase.CreateWorkflowInput{
                    BoardID:    "board-001",
                    Name:       "Test Workflow",
                    OperatorID: "unauthorized-user",
                }
                _, err = uc.Execute(context.Background(), input)
            })

            It("should return AuthorizationError", func() {
                Expect(err).To(MatchError(domain.ErrUnauthorized))
            })

            It("should not create any workflow", func() {
                count, _ := repo.Count(context.Background())
                Expect(count).To(Equal(0))
            })

            It("should not publish any event", func() {
                Expect(eventPub.EventCount()).To(Equal(0))
            })
        })
    })

    // ===== Table-Driven Tests =====
    DescribeTable("Scenario: Validation errors",
        func(boardID, name, expectedError string) {
            authSvc.AllowAll()

            input := usecase.CreateWorkflowInput{
                BoardID:    boardID,
                Name:       name,
                OperatorID: "user-123",
            }
            _, err := uc.Execute(context.Background(), input)

            Expect(err).To(HaveOccurred())
            Expect(err.Error()).To(ContainSubstring(expectedError))
        },
        Entry("empty boardId", "", "Workflow", "boardId is required"),
        Entry("empty name", "board-001", "", "name is required"),
        Entry("name too long", "board-001", string(make([]byte, 256)), "name exceeds max length"),
    )
})

TypeScript: Cucumber.js 生成

// tests/​acceptance/​features/​create-workflow.feature
// Auto-generated from acceptance.yaml

Feature: Create Workflow
  As a board member
  I want to create a workflow for my board
  So that I can organize my work

  @smoke @api @AC1
  Scenario: Create a valid workflow successfully
    Given a boardId "board-001" is provided
    And a user "user-123" is authorized to create workflows for that boardId
    When the user requests to create a workflow with name "First workflow"
    Then the request should succeed
    And a Workflow should be created with name "First workflow"
    And the Workflow should belong to Board "board-001"
    And the Workflow should be active
    And a WorkflowCreated event should be published

  @security @AC2  
  Scenario: Reject workflow creation when not authorized
    Given a boardId "board-001" is provided
    And a user "unauthorized-user" is NOT authorized to create workflows
    When the user attempts to create a workflow
    Then the request should fail with "AuthorizationError"
    And no Workflow should be created
// tests/​acceptance/​steps/​create-workflow.steps.ts
// Auto-generated step definitions for Cucumber.js

import { Given, When, Then, Before, After } from '@cucumber/​cucumber';
import { expect } from 'chai';
import { CreateWorkflowUseCase } from '@/​application/​use-cases/​CreateWorkflowUseCase';
import { InMemoryWorkflowRepository } from '@/​infrastructure/​repositories/​InMemoryWorkflowRepository';
import { MockEventPublisher } from '@/​tests/​mocks/​MockEventPublisher';
import { MockAuthorizationService } from '@/​tests/​mocks/​MockAuthorizationService';

interface World {
  repository: InMemoryWorkflowRepository;
  eventPublisher: MockEventPublisher;
  authService: MockAuthorizationService;
  useCase: CreateWorkflowUseCase;
  input: { boardId: string; name: string; operatorId: string };
  result: { workflowId: string } | null;
  error: Error | null;
}

Before(function (this: World) {
  this.repository = new InMemoryWorkflowRepository();
  this.eventPublisher = new MockEventPublisher();
  this.authService = new MockAuthorizationService();
  this.useCase = new CreateWorkflowUseCase(
    this.repository,
    this.eventPublisher,
    this.authService
  );
  this.result = null;
  this.error = null;
});

// ===== Given Steps =====

Given('a boardId {string} is provided', function (this: World, boardId: string) {
  this.input = { ...this.input, boardId };
});

Given(
  'a user {string} is authorized to create workflows for that boardId',
  function (this: World, userId: string) {
    this.input = { ...this.input, operatorId: userId };
    this.authService.allowCapability(userId, 'create_workflow', this.input.boardId);
  }
);

Given(
  'a user {string} is NOT authorized to create workflows',
  function (this: World, userId: string) {
    this.input = { ...this.input, operatorId: userId };
    this.authService.denyAll();
  }
);

// ===== When Steps =====

When(
  'the user requests to create a workflow with name {string}',
  async function (this: World, name: string) {
    this.input = { ...this.input, name };
    try {
      this.result = await this.useCase.execute(this.input);
    } catch (e) {
      this.error = e as Error;
    }
  }
);

When('the user attempts to create a workflow', async function (this: World) {
  this.input = { ...this.input, name: 'Test Workflow' };
  try {
    this.result = await this.useCase.execute(this.input);
  } catch (e) {
    this.error = e as Error;
  }
});

// ===== Then Steps =====

Then('the request should succeed', function (this: World) {
  expect(this.error).to.be.null;
  expect(this.result).to.not.be.null;
});

Then('the request should fail with {string}', function (this: World, errorType: string) {
  expect(this.error).to.not.be.null;
  expect(this.error!.name).to.equal(errorType);
});

Then(
  'a Workflow should be created with name {string}',
  async function (this: World, name: string) {
    const workflow = await this.repository.findById(this.result!.workflowId);
    expect(workflow).to.not.be.null;
    expect(workflow!.name).to.equal(name);
  }
);

Then(
  'the Workflow should belong to Board {string}',
  async function (this: World, boardId: string) {
    const workflow = await this.repository.findById(this.result!.workflowId);
    expect(workflow!.boardId).to.equal(boardId);
  }
);

Then('the Workflow should be active', async function (this: World) {
  const workflow = await this.repository.findById(this.result!.workflowId);
  expect(workflow!.isDeleted).to.be.false;
});

Then('a WorkflowCreated event should be published', function (this: World) {
  expect(this.eventPublisher.events).to.have.lengthOf(1);
  expect(this.eventPublisher.events[0].type).to.equal('WorkflowCreated');
});

Then('no Workflow should be created', async function (this: World) {
  const count = await this.repository.count();
  expect(count).to.equal(0);
});

TypeScript: Jest-Cucumber 替代方案

// tests/​acceptance/​create-workflow.spec.ts
// Using jest-cucumber for tighter Jest integration

import { defineFeature, loadFeature } from 'jest-cucumber';
import { CreateWorkflowUseCase } from '@/​application/​use-cases/​CreateWorkflowUseCase';
import { InMemoryWorkflowRepository } from '@/​infrastructure/​repositories/​InMemoryWorkflowRepository';
import { MockEventPublisher } from '@/​tests/​mocks/​MockEventPublisher';
import { MockAuthorizationService } from '@/​tests/​mocks/​MockAuthorizationService';

const feature = loadFeature('./​features/​create-workflow.feature');

defineFeature(feature, (test) => {
  let repository: InMemoryWorkflowRepository;
  let eventPublisher: MockEventPublisher;
  let authService: MockAuthorizationService;
  let useCase: CreateWorkflowUseCase;
  let input: { boardId: string; name: string; operatorId: string };
  let result: { workflowId: string } | null;
  let error: Error | null;

  beforeEach(() => {
    repository = new InMemoryWorkflowRepository();
    eventPublisher = new MockEventPublisher();
    authService = new MockAuthorizationService();
    useCase = new CreateWorkflowUseCase(repository, eventPublisher, authService);
    result = null;
    error = null;
  });

  test('Create a valid workflow successfully', ({ given, and, when, then }) => {
    given(/^a boardId "(.*)" is provided$/, (boardId: string) => {
      input = { ...input, boardId };
    });

    and(/^a user "(.*)" is authorized to create workflows for that boardId$/, (userId: string) => {
      input = { ...input, operatorId: userId };
      authService.allowCapability(userId, 'create_workflow', input.boardId);
    });

    when(/^the user requests to create a workflow with name "(.*)"$/, async (name: string) => {
      input = { ...input, name };
      try {
        result = await useCase.execute(input);
      } catch (e) {
        error = e as Error;
      }
    });

    then('the request should succeed', () => {
      expect(error).toBeNull();
      expect(result).not.toBeNull();
    });

    and(/^a Workflow should be created with name "(.*)"$/, async (name: string) => {
      const workflow = await repository.findById(result!.workflowId);
      expect(workflow?.name).toBe(name);
    });

    and('a WorkflowCreated event should be published', () => {
      expect(eventPublisher.events).toHaveLength(1);
      expect(eventPublisher.events[0].type).toBe('WorkflowCreated');
    });
  });
});

Rust: cucumber-rs 生成

// tests/​acceptance/​create_workflow.rs
// Auto-generated from acceptance.yaml
// Framework: cucumber-rs

use cucumber::{given, when, then, World};
use async_trait::async_trait;
use std::sync::Arc;
use tokio::sync::Mutex;

use myapp::application::use_cases::{CreateWorkflowUseCase, CreateWorkflowInput, CreateWorkflowOutput};
use myapp::domain::{WorkflowRepository, EventPublisher, AuthorizationService};
use myapp::domain::errors::DomainError;
use myapp::infrastructure::repositories::InMemoryWorkflowRepository;
use myapp::tests::mocks::{MockEventPublisher, MockAuthorizationService};

// ===== World Definition =====

#[derive(Debug, World)]
#[world(init = Self::new)]
pub struct CreateWorkflowWorld {
    repository: Arc<Mutex<InMemoryWorkflowRepository>>,
    event_publisher: Arc<Mutex<MockEventPublisher>>,
    auth_service: Arc<Mutex<MockAuthorizationService>>,
    input: Option<CreateWorkflowInput>,
    result: Option<Result<CreateWorkflowOutput, DomainError>>,
}

impl CreateWorkflowWorld {
    fn new() -> Self {
        Self {
            repository: Arc::new(Mutex::new(InMemoryWorkflowRepository::new())),
            event_publisher: Arc::new(Mutex::new(MockEventPublisher::new())),
            auth_service: Arc::new(Mutex::new(MockAuthorizationService::new())),
            input: None,
            result: None,
        }
    }
}

// ===== AC1: Create a valid workflow successfully =====
// Trace: CBF-REQ-1
// Frame Concerns: WF-FC-AUTH, FC2

#[given(expr = "a boardId {string} is provided")]
async fn given_board_id(world: &mut CreateWorkflowWorld, board_id: String) {
    world.input = Some(CreateWorkflowInput {
        board_id,
        name: String::new(),
        operator_id: String::new(),
    });
}

#[given(expr = "a user {string} is authorized to create workflows for that boardId")]
async fn given_user_authorized(world: &mut CreateWorkflowWorld, user_id: String) {
    let mut input = world.input.take().unwrap();
    input.operator_id = user_id.clone();
    world.input = Some(input);

    let mut auth = world.auth_service.lock().await;
    auth.allow_capability(&user_id, "create_workflow", &world.input.as_ref().unwrap().board_id);
}

#[given(expr = "a user {string} is NOT authorized to create workflows")]
async fn given_user_not_authorized(world: &mut CreateWorkflowWorld, user_id: String) {
    let mut input = world.input.take().unwrap();
    input.operator_id = user_id;
    world.input = Some(input);

    let mut auth = world.auth_service.lock().await;
    auth.deny_all();
}

#[when(expr = "the user requests to create a workflow with name {string}")]
async fn when_create_workflow(world: &mut CreateWorkflowWorld, name: String) {
    let mut input = world.input.take().unwrap();
    input.name = name;
    world.input = Some(input.clone());

    let use_case = CreateWorkflowUseCase::new(
        world.repository.clone(),
        world.event_publisher.clone(),
        world.auth_service.clone(),
    );

    world.result = Some(use_case.execute(input).await);
}

#[when("the user attempts to create a workflow")]
async fn when_attempt_create(world: &mut CreateWorkflowWorld) {
    let mut input = world.input.take().unwrap();
    input.name = "Test Workflow".to_string();
    world.input = Some(input.clone());

    let use_case = CreateWorkflowUseCase::new(
        world.repository.clone(),
        world.event_publisher.clone(),
        world.auth_service.clone(),
    );

    world.result = Some(use_case.execute(input).await);
}

#[then("the request should succeed")]
async fn then_success(world: &mut CreateWorkflowWorld) {
    assert!(world.result.as_ref().unwrap().is_ok(), "Expected success but got error");
}

#[then(expr = "the request should fail with {string}")]
async fn then_fail_with(world: &mut CreateWorkflowWorld, error_type: String) {
    let result = world.result.as_ref().unwrap();
    assert!(result.is_err(), "Expected error but got success");
    
    let err = result.as_ref().unwrap_err();
    match error_type.as_str() {
        "AuthorizationError" => assert!(matches!(err, DomainError::Unauthorized(_))),
        "ValidationError" => assert!(matches!(err, DomainError::Validation(_))),
        _ => panic!("Unknown error type: {}", error_type),
    }
}

#[then(expr = "a Workflow should be created with name {string}")]
async fn then_workflow_created(world: &mut CreateWorkflowWorld, name: String) {
    let result = world.result.as_ref().unwrap().as_ref().unwrap();
    let repo = world.repository.lock().await;
    let workflow = repo.find_by_id(&result.workflow_id).await.unwrap().unwrap();
    
    assert_eq!(workflow.name, name);
}

#[then(expr = "the Workflow should belong to Board {string}")]
async fn then_workflow_belongs_to_board(world: &mut CreateWorkflowWorld, board_id: String) {
    let result = world.result.as_ref().unwrap().as_ref().unwrap();
    let repo = world.repository.lock().await;
    let workflow = repo.find_by_id(&result.workflow_id).await.unwrap().unwrap();
    
    assert_eq!(workflow.board_id, board_id);
}

#[then("the Workflow should be active")]
async fn then_workflow_active(world: &mut CreateWorkflowWorld) {
    let result = world.result.as_ref().unwrap().as_ref().unwrap();
    let repo = world.repository.lock().await;
    let workflow = repo.find_by_id(&result.workflow_id).await.unwrap().unwrap();
    
    assert!(!workflow.is_deleted);
}

#[then("a WorkflowCreated event should be published")]
async fn then_event_published(world: &mut CreateWorkflowWorld) {
    let publisher = world.event_publisher.lock().await;
    assert_eq!(publisher.event_count(), 1);
    assert!(publisher.has_event_type("WorkflowCreated"));
}

#[then("no Workflow should be created")]
async fn then_no_workflow(world: &mut CreateWorkflowWorld) {
    let repo = world.repository.lock().await;
    assert_eq!(repo.count().await, 0);
}

// ===== Test Runner =====

#[tokio::main]
async fn main() {
    CreateWorkflowWorld::run("tests/​features/​create-workflow.feature").await;
}

Rust: Cargo.toml 依賴

[dev-dependencies]
cucumber = { version = "0.20", features = ["macros"] }
async-trait = "0.1"
tokio = { version = "1", features = ["full", "test-util"] }

框架選擇指南

考量JavaGoTypeScriptRust
推薦框架ezSpecGinkgoCucumber.jscucumber-rs
備選Cucumber-JVMgodogjest-cucumber-
Step Definition自動 (ezSpec)內建手寫宏輔助
非同步支援✅✅✅✅ (async/​await)
表格測試ExamplesDescribeTableScenario OutlineExamples
IDE 支援IntelliJGoLandVS Coderust-analyzer

與其他 Skills 的協作

analyze-frame
    │
    └── 生成 acceptance.yaml
            │
            └── generate-acceptance-test (本 Skill)
                    │
                    ├── 生成 .feature (Gherkin)
                    ├── 生成 Java ezSpec (Fluent API)
                    ├── 生成 Go Ginkgo tests
                    ├── 生成 TypeScript Cucumber steps
                    ├── 生成 Rust cucumber-rs tests
                    │
                    ├── 連結 → enforce-contract (驗證 contracts)
                    └── 連結 → cross-context (驗證 ACL)

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View