generate-acceptance-test

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

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "generate-acceptance-test" with this command: npx skills add knowlet/skills/knowlet-skills-generate-acceptance-test

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)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

spec-compliance-validator

No summary provided by upstream source.

Repository SourceNeeds Review
General

enforce-contract

No summary provided by upstream source.

Repository SourceNeeds Review
General

saga-orchestrator

No summary provided by upstream source.

Repository SourceNeeds Review
General

multi-model-reviewer

No summary provided by upstream source.

Repository SourceNeeds Review