@su-record/vibe 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/explorer.md +48 -48
- package/.claude/agents/implementer.md +53 -53
- package/.claude/agents/searcher.md +54 -54
- package/.claude/agents/simplifier.md +119 -119
- package/.claude/agents/tester.md +49 -49
- package/.claude/commands/vibe.analyze.md +239 -239
- package/.claude/commands/vibe.continue.md +88 -88
- package/.claude/commands/vibe.diagram.md +178 -178
- package/.claude/commands/vibe.reason.md +306 -306
- package/.claude/commands/vibe.run.md +760 -760
- package/.claude/commands/vibe.spec.md +339 -339
- package/.claude/commands/vibe.tool.md +153 -153
- package/.claude/commands/vibe.ui.md +137 -137
- package/.claude/commands/vibe.verify.md +238 -238
- package/.claude/settings.json +152 -152
- package/.claude/settings.local.json +4 -57
- package/.vibe/config.json +9 -0
- package/.vibe/constitution.md +184 -184
- package/.vibe/rules/core/communication-guide.md +104 -104
- package/.vibe/rules/core/development-philosophy.md +52 -52
- package/.vibe/rules/core/quick-start.md +120 -120
- package/.vibe/rules/quality/bdd-contract-testing.md +388 -388
- package/.vibe/rules/quality/checklist.md +276 -276
- package/.vibe/rules/quality/testing-strategy.md +437 -437
- package/.vibe/rules/standards/anti-patterns.md +369 -369
- package/.vibe/rules/standards/code-structure.md +291 -291
- package/.vibe/rules/standards/complexity-metrics.md +312 -312
- package/.vibe/rules/standards/naming-conventions.md +198 -198
- package/.vibe/rules/tools/mcp-hi-ai-guide.md +665 -665
- package/.vibe/rules/tools/mcp-workflow.md +51 -51
- package/.vibe/setup.sh +31 -31
- package/CLAUDE.md +122 -122
- package/LICENSE +21 -21
- package/README.md +568 -568
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +391 -406
- package/dist/cli/index.js.map +1 -1
- package/dist/lib/MemoryManager.js +92 -92
- package/dist/lib/PythonParser.js +108 -108
- package/dist/lib/gemini-mcp.js +15 -15
- package/dist/lib/gemini-oauth.js +35 -35
- package/dist/lib/gpt-mcp.js +17 -17
- package/dist/lib/gpt-oauth.js +44 -44
- package/dist/tools/analytics/getUsageAnalytics.js +12 -12
- package/dist/tools/memory/createMemoryTimeline.js +10 -10
- package/dist/tools/memory/getMemoryGraph.js +12 -12
- package/dist/tools/memory/getSessionContext.js +9 -9
- package/dist/tools/memory/linkMemories.js +14 -14
- package/dist/tools/memory/listMemories.js +4 -4
- package/dist/tools/memory/recallMemory.js +4 -4
- package/dist/tools/memory/saveMemory.js +4 -4
- package/dist/tools/memory/searchMemoriesAdvanced.js +22 -22
- package/dist/tools/planning/generatePrd.js +46 -46
- package/dist/tools/prompt/enhancePromptGemini.js +160 -160
- package/dist/tools/reasoning/applyReasoningFramework.js +56 -56
- package/dist/tools/semantic/analyzeDependencyGraph.js +12 -12
- package/package.json +67 -67
- package/templates/constitution-template.md +184 -184
- package/templates/contract-backend-template.md +517 -517
- package/templates/contract-frontend-template.md +594 -594
- package/templates/feature-template.md +96 -96
- package/templates/hooks-template.json +103 -103
- package/templates/spec-template.md +199 -199
- package/dist/lib/vibe-mcp.d.ts.map +0 -1
- package/dist/lib/vibe-mcp.js.map +0 -1
|
@@ -1,517 +1,517 @@
|
|
|
1
|
-
# Backend Contract Tests: {기능명}
|
|
2
|
-
|
|
3
|
-
**Generated from**: `specs/{기능명}.md` (Section 6: API 계약)
|
|
4
|
-
**Framework**: {FastAPI | Django | Express | NestJS}
|
|
5
|
-
**Language**: {Python | TypeScript | JavaScript}
|
|
6
|
-
**Priority**: {HIGH | MEDIUM | LOW}
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## Overview
|
|
11
|
-
|
|
12
|
-
Contract Testing은 **API 계약(스키마)을 검증**합니다:
|
|
13
|
-
- ✅ Request/Response 스키마 준수
|
|
14
|
-
- ✅ 상태 코드 일치
|
|
15
|
-
- ✅ 헤더 검증
|
|
16
|
-
- ✅ 데이터 타입 및 필수 필드 확인
|
|
17
|
-
|
|
18
|
-
**Consumer → Provider 계약 보장** (Pact 패턴)
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## API Contracts
|
|
23
|
-
|
|
24
|
-
### Contract 1: {엔드포인트 이름}
|
|
25
|
-
|
|
26
|
-
**Endpoint**: `POST /api/v1/{resource}`
|
|
27
|
-
**Mapped to**: REQ-001 in SPEC
|
|
28
|
-
|
|
29
|
-
#### Request Contract
|
|
30
|
-
|
|
31
|
-
```json
|
|
32
|
-
{
|
|
33
|
-
"method": "POST",
|
|
34
|
-
"path": "/api/v1/{resource}",
|
|
35
|
-
"headers": {
|
|
36
|
-
"Content-Type": "application/json",
|
|
37
|
-
"Authorization": "Bearer {token}"
|
|
38
|
-
},
|
|
39
|
-
"body": {
|
|
40
|
-
"field1": "string (required)",
|
|
41
|
-
"field2": "integer (required)",
|
|
42
|
-
"field3": "boolean (optional)"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
**JSON Schema**:
|
|
48
|
-
```json
|
|
49
|
-
{
|
|
50
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
51
|
-
"type": "object",
|
|
52
|
-
"required": ["field1", "field2"],
|
|
53
|
-
"properties": {
|
|
54
|
-
"field1": {
|
|
55
|
-
"type": "string",
|
|
56
|
-
"minLength": 1,
|
|
57
|
-
"maxLength": 100
|
|
58
|
-
},
|
|
59
|
-
"field2": {
|
|
60
|
-
"type": "integer",
|
|
61
|
-
"minimum": 0
|
|
62
|
-
},
|
|
63
|
-
"field3": {
|
|
64
|
-
"type": "boolean",
|
|
65
|
-
"default": false
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
"additionalProperties": false
|
|
69
|
-
}
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
#### Response Contract (Success)
|
|
73
|
-
|
|
74
|
-
```json
|
|
75
|
-
{
|
|
76
|
-
"status": 201,
|
|
77
|
-
"headers": {
|
|
78
|
-
"Content-Type": "application/json"
|
|
79
|
-
},
|
|
80
|
-
"body": {
|
|
81
|
-
"id": "uuid",
|
|
82
|
-
"field1": "string",
|
|
83
|
-
"field2": "integer",
|
|
84
|
-
"field3": "boolean",
|
|
85
|
-
"created_at": "datetime (ISO 8601)"
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**JSON Schema**:
|
|
91
|
-
```json
|
|
92
|
-
{
|
|
93
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
94
|
-
"type": "object",
|
|
95
|
-
"required": ["id", "field1", "field2", "created_at"],
|
|
96
|
-
"properties": {
|
|
97
|
-
"id": {
|
|
98
|
-
"type": "string",
|
|
99
|
-
"format": "uuid"
|
|
100
|
-
},
|
|
101
|
-
"field1": {
|
|
102
|
-
"type": "string"
|
|
103
|
-
},
|
|
104
|
-
"field2": {
|
|
105
|
-
"type": "integer"
|
|
106
|
-
},
|
|
107
|
-
"field3": {
|
|
108
|
-
"type": "boolean"
|
|
109
|
-
},
|
|
110
|
-
"created_at": {
|
|
111
|
-
"type": "string",
|
|
112
|
-
"format": "date-time"
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
#### Response Contract (Error)
|
|
119
|
-
|
|
120
|
-
```json
|
|
121
|
-
{
|
|
122
|
-
"status": 400,
|
|
123
|
-
"body": {
|
|
124
|
-
"error": "string",
|
|
125
|
-
"message": "string",
|
|
126
|
-
"details": ["array of strings (optional)"]
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
---
|
|
132
|
-
|
|
133
|
-
## Implementation
|
|
134
|
-
|
|
135
|
-
### Python (FastAPI + Pydantic)
|
|
136
|
-
|
|
137
|
-
**File**: `tests/contract/test_{기능명}_contract.py`
|
|
138
|
-
|
|
139
|
-
```python
|
|
140
|
-
import pytest
|
|
141
|
-
from fastapi.testclient import TestClient
|
|
142
|
-
from jsonschema import validate, ValidationError
|
|
143
|
-
from app.main import app
|
|
144
|
-
|
|
145
|
-
client = TestClient(app)
|
|
146
|
-
|
|
147
|
-
# JSON Schema definitions
|
|
148
|
-
REQUEST_SCHEMA = {
|
|
149
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
150
|
-
"type": "object",
|
|
151
|
-
"required": ["field1", "field2"],
|
|
152
|
-
"properties": {
|
|
153
|
-
"field1": {"type": "string", "minLength": 1, "maxLength": 100},
|
|
154
|
-
"field2": {"type": "integer", "minimum": 0},
|
|
155
|
-
"field3": {"type": "boolean", "default": False}
|
|
156
|
-
},
|
|
157
|
-
"additionalProperties": False
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
RESPONSE_SCHEMA = {
|
|
161
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
162
|
-
"type": "object",
|
|
163
|
-
"required": ["id", "field1", "field2", "created_at"],
|
|
164
|
-
"properties": {
|
|
165
|
-
"id": {"type": "string", "format": "uuid"},
|
|
166
|
-
"field1": {"type": "string"},
|
|
167
|
-
"field2": {"type": "integer"},
|
|
168
|
-
"field3": {"type": "boolean"},
|
|
169
|
-
"created_at": {"type": "string", "format": "date-time"}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
class TestCreateResourceContract:
|
|
174
|
-
"""Contract tests for POST /api/v1/resource"""
|
|
175
|
-
|
|
176
|
-
def test_request_schema_valid(self):
|
|
177
|
-
"""Request body matches contract schema"""
|
|
178
|
-
payload = {
|
|
179
|
-
"field1": "test value",
|
|
180
|
-
"field2": 42,
|
|
181
|
-
"field3": True
|
|
182
|
-
}
|
|
183
|
-
# Should not raise ValidationError
|
|
184
|
-
validate(instance=payload, schema=REQUEST_SCHEMA)
|
|
185
|
-
|
|
186
|
-
def test_request_schema_invalid_missing_required(self):
|
|
187
|
-
"""Request with missing required field is rejected"""
|
|
188
|
-
payload = {
|
|
189
|
-
"field1": "test value"
|
|
190
|
-
# Missing field2
|
|
191
|
-
}
|
|
192
|
-
with pytest.raises(ValidationError):
|
|
193
|
-
validate(instance=payload, schema=REQUEST_SCHEMA)
|
|
194
|
-
|
|
195
|
-
def test_response_schema_success(self):
|
|
196
|
-
"""Response body matches contract schema (201 Created)"""
|
|
197
|
-
payload = {
|
|
198
|
-
"field1": "test value",
|
|
199
|
-
"field2": 42,
|
|
200
|
-
"field3": True
|
|
201
|
-
}
|
|
202
|
-
response = client.post(
|
|
203
|
-
"/api/v1/resource",
|
|
204
|
-
json=payload,
|
|
205
|
-
headers={"Authorization": "Bearer test-token"}
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
# Status code contract
|
|
209
|
-
assert response.status_code == 201
|
|
210
|
-
|
|
211
|
-
# Response schema contract
|
|
212
|
-
response_data = response.json()
|
|
213
|
-
validate(instance=response_data, schema=RESPONSE_SCHEMA)
|
|
214
|
-
|
|
215
|
-
# Data contract
|
|
216
|
-
assert response_data["field1"] == payload["field1"]
|
|
217
|
-
assert response_data["field2"] == payload["field2"]
|
|
218
|
-
assert response_data["field3"] == payload["field3"]
|
|
219
|
-
|
|
220
|
-
def test_response_schema_error(self):
|
|
221
|
-
"""Error response matches contract schema (400 Bad Request)"""
|
|
222
|
-
payload = {
|
|
223
|
-
"field1": "", # Invalid: empty string
|
|
224
|
-
"field2": -1 # Invalid: negative
|
|
225
|
-
}
|
|
226
|
-
response = client.post(
|
|
227
|
-
"/api/v1/resource",
|
|
228
|
-
json=payload,
|
|
229
|
-
headers={"Authorization": "Bearer test-token"}
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
# Status code contract
|
|
233
|
-
assert response.status_code == 400
|
|
234
|
-
|
|
235
|
-
# Error schema contract
|
|
236
|
-
error_data = response.json()
|
|
237
|
-
assert "error" in error_data
|
|
238
|
-
assert "message" in error_data
|
|
239
|
-
assert isinstance(error_data["message"], str)
|
|
240
|
-
|
|
241
|
-
def test_headers_contract(self):
|
|
242
|
-
"""Response headers match contract"""
|
|
243
|
-
payload = {
|
|
244
|
-
"field1": "test value",
|
|
245
|
-
"field2": 42
|
|
246
|
-
}
|
|
247
|
-
response = client.post(
|
|
248
|
-
"/api/v1/resource",
|
|
249
|
-
json=payload,
|
|
250
|
-
headers={"Authorization": "Bearer test-token"}
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
assert response.headers["Content-Type"] == "application/json"
|
|
254
|
-
|
|
255
|
-
@pytest.mark.parametrize("invalid_payload,expected_error", [
|
|
256
|
-
({"field1": "x" * 101, "field2": 42}, "field1 too long"),
|
|
257
|
-
({"field1": "test", "field2": -1}, "field2 must be positive"),
|
|
258
|
-
({"field2": 42}, "field1 is required"),
|
|
259
|
-
])
|
|
260
|
-
def test_validation_errors(self, invalid_payload, expected_error):
|
|
261
|
-
"""Contract validation errors are properly handled"""
|
|
262
|
-
response = client.post(
|
|
263
|
-
"/api/v1/resource",
|
|
264
|
-
json=invalid_payload,
|
|
265
|
-
headers={"Authorization": "Bearer test-token"}
|
|
266
|
-
)
|
|
267
|
-
assert response.status_code == 400
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
**Run**:
|
|
271
|
-
```bash
|
|
272
|
-
pytest tests/contract/test_{기능명}_contract.py -v --tb=short
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
---
|
|
276
|
-
|
|
277
|
-
### Python (Pact - Consumer-Driven Contracts)
|
|
278
|
-
|
|
279
|
-
**File**: `tests/pact/consumer_test_{기능명}.py`
|
|
280
|
-
|
|
281
|
-
```python
|
|
282
|
-
import pytest
|
|
283
|
-
from pact import Consumer, Provider, Like, EachLike, Format
|
|
284
|
-
|
|
285
|
-
pact = Consumer('FrontendApp').has_pact_with(Provider('BackendAPI'))
|
|
286
|
-
|
|
287
|
-
@pytest.fixture(scope='module')
|
|
288
|
-
def setup_pact():
|
|
289
|
-
pact.start_service()
|
|
290
|
-
yield
|
|
291
|
-
pact.stop_service()
|
|
292
|
-
|
|
293
|
-
def test_create_resource_contract(setup_pact):
|
|
294
|
-
"""Consumer expects provider to create resource"""
|
|
295
|
-
expected = {
|
|
296
|
-
'id': Format().uuid,
|
|
297
|
-
'field1': Like('test value'),
|
|
298
|
-
'field2': Like(42),
|
|
299
|
-
'field3': Like(True),
|
|
300
|
-
'created_at': Format().iso_8601_datetime
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
(pact
|
|
304
|
-
.given('user is authenticated')
|
|
305
|
-
.upon_receiving('a request to create resource')
|
|
306
|
-
.with_request('POST', '/api/v1/resource',
|
|
307
|
-
headers={'Authorization': Like('Bearer token')},
|
|
308
|
-
body={
|
|
309
|
-
'field1': 'test value',
|
|
310
|
-
'field2': 42,
|
|
311
|
-
'field3': True
|
|
312
|
-
})
|
|
313
|
-
.will_respond_with(201, body=expected))
|
|
314
|
-
|
|
315
|
-
with pact:
|
|
316
|
-
# Test consumer code
|
|
317
|
-
result = api_client.create_resource(field1='test value', field2=42)
|
|
318
|
-
assert result['id'] is not None
|
|
319
|
-
assert result['field1'] == 'test value'
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
**Generate Pact file**:
|
|
323
|
-
```bash
|
|
324
|
-
pytest tests/pact/ --pact-broker-url=https://your-pact-broker.com
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
---
|
|
328
|
-
|
|
329
|
-
### TypeScript (NestJS + Jest)
|
|
330
|
-
|
|
331
|
-
**File**: `test/contract/{기능명}.contract.spec.ts`
|
|
332
|
-
|
|
333
|
-
```typescript
|
|
334
|
-
import { Test } from '@nestjs/testing';
|
|
335
|
-
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
|
336
|
-
import * as request from 'supertest';
|
|
337
|
-
import { AppModule } from '../src/app.module';
|
|
338
|
-
import Ajv from 'ajv';
|
|
339
|
-
import addFormats from 'ajv-formats';
|
|
340
|
-
|
|
341
|
-
describe('Create Resource Contract (e2e)', () => {
|
|
342
|
-
let app: INestApplication;
|
|
343
|
-
const ajv = new Ajv();
|
|
344
|
-
addFormats(ajv);
|
|
345
|
-
|
|
346
|
-
const requestSchema = {
|
|
347
|
-
type: 'object',
|
|
348
|
-
required: ['field1', 'field2'],
|
|
349
|
-
properties: {
|
|
350
|
-
field1: { type: 'string', minLength: 1, maxLength: 100 },
|
|
351
|
-
field2: { type: 'integer', minimum: 0 },
|
|
352
|
-
field3: { type: 'boolean' }
|
|
353
|
-
},
|
|
354
|
-
additionalProperties: false
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
const responseSchema = {
|
|
358
|
-
type: 'object',
|
|
359
|
-
required: ['id', 'field1', 'field2', 'createdAt'],
|
|
360
|
-
properties: {
|
|
361
|
-
id: { type: 'string', format: 'uuid' },
|
|
362
|
-
field1: { type: 'string' },
|
|
363
|
-
field2: { type: 'integer' },
|
|
364
|
-
field3: { type: 'boolean' },
|
|
365
|
-
createdAt: { type: 'string', format: 'date-time' }
|
|
366
|
-
}
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
beforeAll(async () => {
|
|
370
|
-
const moduleFixture = await Test.createTestingModule({
|
|
371
|
-
imports: [AppModule],
|
|
372
|
-
}).compile();
|
|
373
|
-
|
|
374
|
-
app = moduleFixture.createNestApplication();
|
|
375
|
-
app.useGlobalPipes(new ValidationPipe());
|
|
376
|
-
await app.init();
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
afterAll(async () => {
|
|
380
|
-
await app.close();
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
it('POST /api/v1/resource - validates request schema', () => {
|
|
384
|
-
const payload = {
|
|
385
|
-
field1: 'test value',
|
|
386
|
-
field2: 42,
|
|
387
|
-
field3: true
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
const validate = ajv.compile(requestSchema);
|
|
391
|
-
expect(validate(payload)).toBe(true);
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
it('POST /api/v1/resource - validates response schema (201)', async () => {
|
|
395
|
-
const response = await request(app.getHttpServer())
|
|
396
|
-
.post('/api/v1/resource')
|
|
397
|
-
.set('Authorization', 'Bearer test-token')
|
|
398
|
-
.send({
|
|
399
|
-
field1: 'test value',
|
|
400
|
-
field2: 42,
|
|
401
|
-
field3: true
|
|
402
|
-
})
|
|
403
|
-
.expect(201)
|
|
404
|
-
.expect('Content-Type', /json/);
|
|
405
|
-
|
|
406
|
-
const validate = ajv.compile(responseSchema);
|
|
407
|
-
expect(validate(response.body)).toBe(true);
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
it('POST /api/v1/resource - returns 400 for invalid request', async () => {
|
|
411
|
-
await request(app.getHttpServer())
|
|
412
|
-
.post('/api/v1/resource')
|
|
413
|
-
.set('Authorization', 'Bearer test-token')
|
|
414
|
-
.send({
|
|
415
|
-
field1: '', // Invalid
|
|
416
|
-
field2: -1 // Invalid
|
|
417
|
-
})
|
|
418
|
-
.expect(400);
|
|
419
|
-
});
|
|
420
|
-
});
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
**Run**:
|
|
424
|
-
```bash
|
|
425
|
-
npm test -- test/contract/{기능명}.contract.spec.ts
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
---
|
|
429
|
-
|
|
430
|
-
## Contract Testing Strategy
|
|
431
|
-
|
|
432
|
-
### 1. Provider Tests (Backend)
|
|
433
|
-
```bash
|
|
434
|
-
# Run all contract tests
|
|
435
|
-
pytest tests/contract/ -v
|
|
436
|
-
|
|
437
|
-
# Run specific contract
|
|
438
|
-
pytest tests/contract/test_{기능명}_contract.py
|
|
439
|
-
|
|
440
|
-
# Generate Pact file for consumer
|
|
441
|
-
pytest tests/pact/ --pact-broker-url=...
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
### 2. Consumer Tests (Frontend)
|
|
445
|
-
```bash
|
|
446
|
-
# Verify against provider contract
|
|
447
|
-
npm run test:contract -- --pact-broker-url=...
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
### 3. CI/CD Integration
|
|
451
|
-
```yaml
|
|
452
|
-
# .github/workflows/contract-tests.yml
|
|
453
|
-
name: Contract Tests
|
|
454
|
-
|
|
455
|
-
on: [pull_request]
|
|
456
|
-
|
|
457
|
-
jobs:
|
|
458
|
-
contract-tests:
|
|
459
|
-
runs-on: ubuntu-latest
|
|
460
|
-
steps:
|
|
461
|
-
- uses: actions/checkout@v2
|
|
462
|
-
- name: Run provider contract tests
|
|
463
|
-
run: pytest tests/contract/ -v
|
|
464
|
-
- name: Publish Pact
|
|
465
|
-
run: pytest tests/pact/ --pact-broker-url=${{ secrets.PACT_BROKER_URL }}
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
---
|
|
469
|
-
|
|
470
|
-
## Coverage Mapping
|
|
471
|
-
|
|
472
|
-
| Contract | SPEC REQ | Endpoints | Status |
|
|
473
|
-
|----------|----------|-----------|--------|
|
|
474
|
-
| Create Resource | REQ-001 | POST /api/v1/resource | ⬜ |
|
|
475
|
-
| Get Resource | REQ-002 | GET /api/v1/resource/:id | ⬜ |
|
|
476
|
-
| Update Resource | REQ-003 | PATCH /api/v1/resource/:id | ⬜ |
|
|
477
|
-
|
|
478
|
-
**Coverage**: 0 / {총 계약 수} (0%)
|
|
479
|
-
|
|
480
|
-
---
|
|
481
|
-
|
|
482
|
-
## Best Practices
|
|
483
|
-
|
|
484
|
-
1. **Test Contract, Not Implementation**
|
|
485
|
-
- ✅ 스키마 준수 확인
|
|
486
|
-
- ❌ 비즈니스 로직 테스트 금지
|
|
487
|
-
|
|
488
|
-
2. **Provider-First vs Consumer-First**
|
|
489
|
-
- Provider-First: API 먼저 정의 → Contract 테스트 작성
|
|
490
|
-
- Consumer-First: Frontend 요구사항 → Pact 작성 → Provider 구현
|
|
491
|
-
|
|
492
|
-
3. **Version Control**
|
|
493
|
-
- API 버전별 Contract 파일 관리
|
|
494
|
-
- Breaking Changes 감지
|
|
495
|
-
|
|
496
|
-
4. **Pact Broker 활용**
|
|
497
|
-
- Contract 중앙 관리
|
|
498
|
-
- Consumer-Provider 매칭
|
|
499
|
-
- CI/CD 자동화
|
|
500
|
-
|
|
501
|
-
---
|
|
502
|
-
|
|
503
|
-
## Next Steps
|
|
504
|
-
|
|
505
|
-
```bash
|
|
506
|
-
# 1. Contract 테스트 작성
|
|
507
|
-
vibe contract "{기능명}"
|
|
508
|
-
|
|
509
|
-
# 2. Provider 구현
|
|
510
|
-
vibe run "Task 1-1"
|
|
511
|
-
|
|
512
|
-
# 3. Contract 검증
|
|
513
|
-
vibe test "{기능명}" --contract
|
|
514
|
-
|
|
515
|
-
# 4. Pact 발행 (선택)
|
|
516
|
-
pytest tests/pact/ --pact-broker-url=...
|
|
517
|
-
```
|
|
1
|
+
# Backend Contract Tests: {기능명}
|
|
2
|
+
|
|
3
|
+
**Generated from**: `specs/{기능명}.md` (Section 6: API 계약)
|
|
4
|
+
**Framework**: {FastAPI | Django | Express | NestJS}
|
|
5
|
+
**Language**: {Python | TypeScript | JavaScript}
|
|
6
|
+
**Priority**: {HIGH | MEDIUM | LOW}
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Contract Testing은 **API 계약(스키마)을 검증**합니다:
|
|
13
|
+
- ✅ Request/Response 스키마 준수
|
|
14
|
+
- ✅ 상태 코드 일치
|
|
15
|
+
- ✅ 헤더 검증
|
|
16
|
+
- ✅ 데이터 타입 및 필수 필드 확인
|
|
17
|
+
|
|
18
|
+
**Consumer → Provider 계약 보장** (Pact 패턴)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## API Contracts
|
|
23
|
+
|
|
24
|
+
### Contract 1: {엔드포인트 이름}
|
|
25
|
+
|
|
26
|
+
**Endpoint**: `POST /api/v1/{resource}`
|
|
27
|
+
**Mapped to**: REQ-001 in SPEC
|
|
28
|
+
|
|
29
|
+
#### Request Contract
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"method": "POST",
|
|
34
|
+
"path": "/api/v1/{resource}",
|
|
35
|
+
"headers": {
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
"Authorization": "Bearer {token}"
|
|
38
|
+
},
|
|
39
|
+
"body": {
|
|
40
|
+
"field1": "string (required)",
|
|
41
|
+
"field2": "integer (required)",
|
|
42
|
+
"field3": "boolean (optional)"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**JSON Schema**:
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
51
|
+
"type": "object",
|
|
52
|
+
"required": ["field1", "field2"],
|
|
53
|
+
"properties": {
|
|
54
|
+
"field1": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"minLength": 1,
|
|
57
|
+
"maxLength": 100
|
|
58
|
+
},
|
|
59
|
+
"field2": {
|
|
60
|
+
"type": "integer",
|
|
61
|
+
"minimum": 0
|
|
62
|
+
},
|
|
63
|
+
"field3": {
|
|
64
|
+
"type": "boolean",
|
|
65
|
+
"default": false
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"additionalProperties": false
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### Response Contract (Success)
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"status": 201,
|
|
77
|
+
"headers": {
|
|
78
|
+
"Content-Type": "application/json"
|
|
79
|
+
},
|
|
80
|
+
"body": {
|
|
81
|
+
"id": "uuid",
|
|
82
|
+
"field1": "string",
|
|
83
|
+
"field2": "integer",
|
|
84
|
+
"field3": "boolean",
|
|
85
|
+
"created_at": "datetime (ISO 8601)"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**JSON Schema**:
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
94
|
+
"type": "object",
|
|
95
|
+
"required": ["id", "field1", "field2", "created_at"],
|
|
96
|
+
"properties": {
|
|
97
|
+
"id": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"format": "uuid"
|
|
100
|
+
},
|
|
101
|
+
"field1": {
|
|
102
|
+
"type": "string"
|
|
103
|
+
},
|
|
104
|
+
"field2": {
|
|
105
|
+
"type": "integer"
|
|
106
|
+
},
|
|
107
|
+
"field3": {
|
|
108
|
+
"type": "boolean"
|
|
109
|
+
},
|
|
110
|
+
"created_at": {
|
|
111
|
+
"type": "string",
|
|
112
|
+
"format": "date-time"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### Response Contract (Error)
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"status": 400,
|
|
123
|
+
"body": {
|
|
124
|
+
"error": "string",
|
|
125
|
+
"message": "string",
|
|
126
|
+
"details": ["array of strings (optional)"]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Implementation
|
|
134
|
+
|
|
135
|
+
### Python (FastAPI + Pydantic)
|
|
136
|
+
|
|
137
|
+
**File**: `tests/contract/test_{기능명}_contract.py`
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
import pytest
|
|
141
|
+
from fastapi.testclient import TestClient
|
|
142
|
+
from jsonschema import validate, ValidationError
|
|
143
|
+
from app.main import app
|
|
144
|
+
|
|
145
|
+
client = TestClient(app)
|
|
146
|
+
|
|
147
|
+
# JSON Schema definitions
|
|
148
|
+
REQUEST_SCHEMA = {
|
|
149
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
150
|
+
"type": "object",
|
|
151
|
+
"required": ["field1", "field2"],
|
|
152
|
+
"properties": {
|
|
153
|
+
"field1": {"type": "string", "minLength": 1, "maxLength": 100},
|
|
154
|
+
"field2": {"type": "integer", "minimum": 0},
|
|
155
|
+
"field3": {"type": "boolean", "default": False}
|
|
156
|
+
},
|
|
157
|
+
"additionalProperties": False
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
RESPONSE_SCHEMA = {
|
|
161
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
162
|
+
"type": "object",
|
|
163
|
+
"required": ["id", "field1", "field2", "created_at"],
|
|
164
|
+
"properties": {
|
|
165
|
+
"id": {"type": "string", "format": "uuid"},
|
|
166
|
+
"field1": {"type": "string"},
|
|
167
|
+
"field2": {"type": "integer"},
|
|
168
|
+
"field3": {"type": "boolean"},
|
|
169
|
+
"created_at": {"type": "string", "format": "date-time"}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
class TestCreateResourceContract:
|
|
174
|
+
"""Contract tests for POST /api/v1/resource"""
|
|
175
|
+
|
|
176
|
+
def test_request_schema_valid(self):
|
|
177
|
+
"""Request body matches contract schema"""
|
|
178
|
+
payload = {
|
|
179
|
+
"field1": "test value",
|
|
180
|
+
"field2": 42,
|
|
181
|
+
"field3": True
|
|
182
|
+
}
|
|
183
|
+
# Should not raise ValidationError
|
|
184
|
+
validate(instance=payload, schema=REQUEST_SCHEMA)
|
|
185
|
+
|
|
186
|
+
def test_request_schema_invalid_missing_required(self):
|
|
187
|
+
"""Request with missing required field is rejected"""
|
|
188
|
+
payload = {
|
|
189
|
+
"field1": "test value"
|
|
190
|
+
# Missing field2
|
|
191
|
+
}
|
|
192
|
+
with pytest.raises(ValidationError):
|
|
193
|
+
validate(instance=payload, schema=REQUEST_SCHEMA)
|
|
194
|
+
|
|
195
|
+
def test_response_schema_success(self):
|
|
196
|
+
"""Response body matches contract schema (201 Created)"""
|
|
197
|
+
payload = {
|
|
198
|
+
"field1": "test value",
|
|
199
|
+
"field2": 42,
|
|
200
|
+
"field3": True
|
|
201
|
+
}
|
|
202
|
+
response = client.post(
|
|
203
|
+
"/api/v1/resource",
|
|
204
|
+
json=payload,
|
|
205
|
+
headers={"Authorization": "Bearer test-token"}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Status code contract
|
|
209
|
+
assert response.status_code == 201
|
|
210
|
+
|
|
211
|
+
# Response schema contract
|
|
212
|
+
response_data = response.json()
|
|
213
|
+
validate(instance=response_data, schema=RESPONSE_SCHEMA)
|
|
214
|
+
|
|
215
|
+
# Data contract
|
|
216
|
+
assert response_data["field1"] == payload["field1"]
|
|
217
|
+
assert response_data["field2"] == payload["field2"]
|
|
218
|
+
assert response_data["field3"] == payload["field3"]
|
|
219
|
+
|
|
220
|
+
def test_response_schema_error(self):
|
|
221
|
+
"""Error response matches contract schema (400 Bad Request)"""
|
|
222
|
+
payload = {
|
|
223
|
+
"field1": "", # Invalid: empty string
|
|
224
|
+
"field2": -1 # Invalid: negative
|
|
225
|
+
}
|
|
226
|
+
response = client.post(
|
|
227
|
+
"/api/v1/resource",
|
|
228
|
+
json=payload,
|
|
229
|
+
headers={"Authorization": "Bearer test-token"}
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Status code contract
|
|
233
|
+
assert response.status_code == 400
|
|
234
|
+
|
|
235
|
+
# Error schema contract
|
|
236
|
+
error_data = response.json()
|
|
237
|
+
assert "error" in error_data
|
|
238
|
+
assert "message" in error_data
|
|
239
|
+
assert isinstance(error_data["message"], str)
|
|
240
|
+
|
|
241
|
+
def test_headers_contract(self):
|
|
242
|
+
"""Response headers match contract"""
|
|
243
|
+
payload = {
|
|
244
|
+
"field1": "test value",
|
|
245
|
+
"field2": 42
|
|
246
|
+
}
|
|
247
|
+
response = client.post(
|
|
248
|
+
"/api/v1/resource",
|
|
249
|
+
json=payload,
|
|
250
|
+
headers={"Authorization": "Bearer test-token"}
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
assert response.headers["Content-Type"] == "application/json"
|
|
254
|
+
|
|
255
|
+
@pytest.mark.parametrize("invalid_payload,expected_error", [
|
|
256
|
+
({"field1": "x" * 101, "field2": 42}, "field1 too long"),
|
|
257
|
+
({"field1": "test", "field2": -1}, "field2 must be positive"),
|
|
258
|
+
({"field2": 42}, "field1 is required"),
|
|
259
|
+
])
|
|
260
|
+
def test_validation_errors(self, invalid_payload, expected_error):
|
|
261
|
+
"""Contract validation errors are properly handled"""
|
|
262
|
+
response = client.post(
|
|
263
|
+
"/api/v1/resource",
|
|
264
|
+
json=invalid_payload,
|
|
265
|
+
headers={"Authorization": "Bearer test-token"}
|
|
266
|
+
)
|
|
267
|
+
assert response.status_code == 400
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Run**:
|
|
271
|
+
```bash
|
|
272
|
+
pytest tests/contract/test_{기능명}_contract.py -v --tb=short
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
### Python (Pact - Consumer-Driven Contracts)
|
|
278
|
+
|
|
279
|
+
**File**: `tests/pact/consumer_test_{기능명}.py`
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
import pytest
|
|
283
|
+
from pact import Consumer, Provider, Like, EachLike, Format
|
|
284
|
+
|
|
285
|
+
pact = Consumer('FrontendApp').has_pact_with(Provider('BackendAPI'))
|
|
286
|
+
|
|
287
|
+
@pytest.fixture(scope='module')
|
|
288
|
+
def setup_pact():
|
|
289
|
+
pact.start_service()
|
|
290
|
+
yield
|
|
291
|
+
pact.stop_service()
|
|
292
|
+
|
|
293
|
+
def test_create_resource_contract(setup_pact):
|
|
294
|
+
"""Consumer expects provider to create resource"""
|
|
295
|
+
expected = {
|
|
296
|
+
'id': Format().uuid,
|
|
297
|
+
'field1': Like('test value'),
|
|
298
|
+
'field2': Like(42),
|
|
299
|
+
'field3': Like(True),
|
|
300
|
+
'created_at': Format().iso_8601_datetime
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
(pact
|
|
304
|
+
.given('user is authenticated')
|
|
305
|
+
.upon_receiving('a request to create resource')
|
|
306
|
+
.with_request('POST', '/api/v1/resource',
|
|
307
|
+
headers={'Authorization': Like('Bearer token')},
|
|
308
|
+
body={
|
|
309
|
+
'field1': 'test value',
|
|
310
|
+
'field2': 42,
|
|
311
|
+
'field3': True
|
|
312
|
+
})
|
|
313
|
+
.will_respond_with(201, body=expected))
|
|
314
|
+
|
|
315
|
+
with pact:
|
|
316
|
+
# Test consumer code
|
|
317
|
+
result = api_client.create_resource(field1='test value', field2=42)
|
|
318
|
+
assert result['id'] is not None
|
|
319
|
+
assert result['field1'] == 'test value'
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Generate Pact file**:
|
|
323
|
+
```bash
|
|
324
|
+
pytest tests/pact/ --pact-broker-url=https://your-pact-broker.com
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
### TypeScript (NestJS + Jest)
|
|
330
|
+
|
|
331
|
+
**File**: `test/contract/{기능명}.contract.spec.ts`
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { Test } from '@nestjs/testing';
|
|
335
|
+
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
|
336
|
+
import * as request from 'supertest';
|
|
337
|
+
import { AppModule } from '../src/app.module';
|
|
338
|
+
import Ajv from 'ajv';
|
|
339
|
+
import addFormats from 'ajv-formats';
|
|
340
|
+
|
|
341
|
+
describe('Create Resource Contract (e2e)', () => {
|
|
342
|
+
let app: INestApplication;
|
|
343
|
+
const ajv = new Ajv();
|
|
344
|
+
addFormats(ajv);
|
|
345
|
+
|
|
346
|
+
const requestSchema = {
|
|
347
|
+
type: 'object',
|
|
348
|
+
required: ['field1', 'field2'],
|
|
349
|
+
properties: {
|
|
350
|
+
field1: { type: 'string', minLength: 1, maxLength: 100 },
|
|
351
|
+
field2: { type: 'integer', minimum: 0 },
|
|
352
|
+
field3: { type: 'boolean' }
|
|
353
|
+
},
|
|
354
|
+
additionalProperties: false
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const responseSchema = {
|
|
358
|
+
type: 'object',
|
|
359
|
+
required: ['id', 'field1', 'field2', 'createdAt'],
|
|
360
|
+
properties: {
|
|
361
|
+
id: { type: 'string', format: 'uuid' },
|
|
362
|
+
field1: { type: 'string' },
|
|
363
|
+
field2: { type: 'integer' },
|
|
364
|
+
field3: { type: 'boolean' },
|
|
365
|
+
createdAt: { type: 'string', format: 'date-time' }
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
beforeAll(async () => {
|
|
370
|
+
const moduleFixture = await Test.createTestingModule({
|
|
371
|
+
imports: [AppModule],
|
|
372
|
+
}).compile();
|
|
373
|
+
|
|
374
|
+
app = moduleFixture.createNestApplication();
|
|
375
|
+
app.useGlobalPipes(new ValidationPipe());
|
|
376
|
+
await app.init();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
afterAll(async () => {
|
|
380
|
+
await app.close();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('POST /api/v1/resource - validates request schema', () => {
|
|
384
|
+
const payload = {
|
|
385
|
+
field1: 'test value',
|
|
386
|
+
field2: 42,
|
|
387
|
+
field3: true
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const validate = ajv.compile(requestSchema);
|
|
391
|
+
expect(validate(payload)).toBe(true);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('POST /api/v1/resource - validates response schema (201)', async () => {
|
|
395
|
+
const response = await request(app.getHttpServer())
|
|
396
|
+
.post('/api/v1/resource')
|
|
397
|
+
.set('Authorization', 'Bearer test-token')
|
|
398
|
+
.send({
|
|
399
|
+
field1: 'test value',
|
|
400
|
+
field2: 42,
|
|
401
|
+
field3: true
|
|
402
|
+
})
|
|
403
|
+
.expect(201)
|
|
404
|
+
.expect('Content-Type', /json/);
|
|
405
|
+
|
|
406
|
+
const validate = ajv.compile(responseSchema);
|
|
407
|
+
expect(validate(response.body)).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('POST /api/v1/resource - returns 400 for invalid request', async () => {
|
|
411
|
+
await request(app.getHttpServer())
|
|
412
|
+
.post('/api/v1/resource')
|
|
413
|
+
.set('Authorization', 'Bearer test-token')
|
|
414
|
+
.send({
|
|
415
|
+
field1: '', // Invalid
|
|
416
|
+
field2: -1 // Invalid
|
|
417
|
+
})
|
|
418
|
+
.expect(400);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**Run**:
|
|
424
|
+
```bash
|
|
425
|
+
npm test -- test/contract/{기능명}.contract.spec.ts
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Contract Testing Strategy
|
|
431
|
+
|
|
432
|
+
### 1. Provider Tests (Backend)
|
|
433
|
+
```bash
|
|
434
|
+
# Run all contract tests
|
|
435
|
+
pytest tests/contract/ -v
|
|
436
|
+
|
|
437
|
+
# Run specific contract
|
|
438
|
+
pytest tests/contract/test_{기능명}_contract.py
|
|
439
|
+
|
|
440
|
+
# Generate Pact file for consumer
|
|
441
|
+
pytest tests/pact/ --pact-broker-url=...
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### 2. Consumer Tests (Frontend)
|
|
445
|
+
```bash
|
|
446
|
+
# Verify against provider contract
|
|
447
|
+
npm run test:contract -- --pact-broker-url=...
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### 3. CI/CD Integration
|
|
451
|
+
```yaml
|
|
452
|
+
# .github/workflows/contract-tests.yml
|
|
453
|
+
name: Contract Tests
|
|
454
|
+
|
|
455
|
+
on: [pull_request]
|
|
456
|
+
|
|
457
|
+
jobs:
|
|
458
|
+
contract-tests:
|
|
459
|
+
runs-on: ubuntu-latest
|
|
460
|
+
steps:
|
|
461
|
+
- uses: actions/checkout@v2
|
|
462
|
+
- name: Run provider contract tests
|
|
463
|
+
run: pytest tests/contract/ -v
|
|
464
|
+
- name: Publish Pact
|
|
465
|
+
run: pytest tests/pact/ --pact-broker-url=${{ secrets.PACT_BROKER_URL }}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Coverage Mapping
|
|
471
|
+
|
|
472
|
+
| Contract | SPEC REQ | Endpoints | Status |
|
|
473
|
+
|----------|----------|-----------|--------|
|
|
474
|
+
| Create Resource | REQ-001 | POST /api/v1/resource | ⬜ |
|
|
475
|
+
| Get Resource | REQ-002 | GET /api/v1/resource/:id | ⬜ |
|
|
476
|
+
| Update Resource | REQ-003 | PATCH /api/v1/resource/:id | ⬜ |
|
|
477
|
+
|
|
478
|
+
**Coverage**: 0 / {총 계약 수} (0%)
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Best Practices
|
|
483
|
+
|
|
484
|
+
1. **Test Contract, Not Implementation**
|
|
485
|
+
- ✅ 스키마 준수 확인
|
|
486
|
+
- ❌ 비즈니스 로직 테스트 금지
|
|
487
|
+
|
|
488
|
+
2. **Provider-First vs Consumer-First**
|
|
489
|
+
- Provider-First: API 먼저 정의 → Contract 테스트 작성
|
|
490
|
+
- Consumer-First: Frontend 요구사항 → Pact 작성 → Provider 구현
|
|
491
|
+
|
|
492
|
+
3. **Version Control**
|
|
493
|
+
- API 버전별 Contract 파일 관리
|
|
494
|
+
- Breaking Changes 감지
|
|
495
|
+
|
|
496
|
+
4. **Pact Broker 활용**
|
|
497
|
+
- Contract 중앙 관리
|
|
498
|
+
- Consumer-Provider 매칭
|
|
499
|
+
- CI/CD 자동화
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Next Steps
|
|
504
|
+
|
|
505
|
+
```bash
|
|
506
|
+
# 1. Contract 테스트 작성
|
|
507
|
+
vibe contract "{기능명}"
|
|
508
|
+
|
|
509
|
+
# 2. Provider 구현
|
|
510
|
+
vibe run "Task 1-1"
|
|
511
|
+
|
|
512
|
+
# 3. Contract 검증
|
|
513
|
+
vibe test "{기능명}" --contract
|
|
514
|
+
|
|
515
|
+
# 4. Pact 발행 (선택)
|
|
516
|
+
pytest tests/pact/ --pact-broker-url=...
|
|
517
|
+
```
|