
generate-acceptance-test
by knowlet
A highly sophisticated, theoretically sound adaptation of Problem Frames for modern Agentic Software Engineering.
SKILL.md
name: generate-acceptance-test description: 從規格目錄的 acceptance.yaml 生成/維護 BDD/ezSpec 測試。使用類似 Gherkin 語法,AI 自動產生 step definition(開發人員不需要手寫),驗收測試規格即為 Executable Specification。
Generate Acceptance Test Skill
觸發時機
- analyze-frame 產生規格目錄後
- acceptance.yaml 更新時
- 需求異動需同步更新驗收測試時
- 代碼生成前,希望先鎖定可執行的驗收測試
核心任務
- 解析 acceptance.yaml 的測試規格
- 自動生成 ezSpec step definition(開發人員不需要手寫)
- 維護規格與測試的一致性
- 建立與 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/
支援語言:
| Language | Flag | Output |
|---|---|---|
| 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 生成規則
-
方法命名:
should_{action}_{outcome}格式 -
JavaDoc 註解:包含 AC ID 和 Validates 項目
-
Fluent API:
.Given()→.When()→.ThenSuccess()/.ThenFailure()→.And()→.Execute() -
Lambda 環境:使用
ScenarioEnvironment傳遞狀態 -
事件驗證:使用
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 會:
- 偵測變更:比對 acceptance.yaml 的變更
- 標記過時測試:在 .feature 檔案中標記需更新的場景
- 生成差異報告:列出需要同步的項目
# 同步狀態報告
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 框架:
| 語言 | 框架 | 特點 |
|---|---|---|
| Java | ezSpec | Fluent API, 無需 step definition |
| Go | Ginkgo + Gomega | BDD 風格, 表格驅動測試 |
| TypeScript | Cucumber.js / Jest-Cucumber | Gherkin 原生支援 |
| Rust | cucumber-rs | Async 支援, 宏輔助 |
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"] }
框架選擇指南
| 考量 | Java | Go | TypeScript | Rust |
|---|---|---|---|---|
| 推薦框架 | ezSpec | Ginkgo | Cucumber.js | cucumber-rs |
| 備選 | Cucumber-JVM | godog | jest-cucumber | - |
| Step Definition | 自動 (ezSpec) | 內建 | 手寫 | 宏輔助 |
| 非同步支援 | ✅ | ✅ | ✅ | ✅ (async/await) |
| 表格測試 | Examples | DescribeTable | Scenario Outline | Examples |
| IDE 支援 | IntelliJ | GoLand | VS Code | rust-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)
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon
