Test interactions between components, services, and external systems to verify they work together correctly.
/plugin marketplace add marcel-Ngan/ai-dev-team/plugin install marcel-ngan-ai-dev-team@marcel-Ngan/ai-dev-teamThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Test interactions between components, services, and external systems to verify they work together correctly.
In our TDD workflow, QA Engineer writes integration tests BEFORE implementation (Red phase).
| Phase | Action | Test Status |
|---|---|---|
| Red | QA Engineer writes integration tests | Tests FAIL (no code yet) |
| Green | Junior Dev implements code | Tests PASS |
| Verify | QA runs tests at QA gate | 100% pass rate required |
Integration tests are owned by QA Engineer, not developers.
/\
/E2E\ <- Full system (few)
/------\
/Integration\ <- Component interaction (some)
/------------\
/ Unit Tests \<- Isolated logic (many)
/________________\
| Type | Scope | Examples |
|---|---|---|
| Component | Module interactions | Service + Repository |
| API | HTTP endpoints | REST/GraphQL endpoints |
| Database | Data persistence | CRUD operations |
| External | Third-party services | Payment, Auth providers |
| Contract | API contracts | Consumer-driven contracts |
describe('{{Endpoint}} API', () => {
let app: Express;
let db: TestDatabase;
let authToken: string;
beforeAll(async () => {
db = await createTestDatabase();
app = createApp({ database: db });
authToken = await getTestAuthToken();
});
afterAll(async () => {
await db.close();
});
beforeEach(async () => {
await db.clear();
await db.seed(testFixtures);
});
describe('GET {{path}}', () => {
it('should return {{resource}} list', async () => {
const response = await request(app)
.get('{{path}}')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body).toMatchObject({
data: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
{{field}}: expect.any({{Type}}),
}),
]),
pagination: {
total: expect.any(Number),
page: 1,
},
});
});
it('should return 401 without auth', async () => {
await request(app)
.get('{{path}}')
.expect(401);
});
});
describe('POST {{path}}', () => {
it('should create {{resource}} with valid data', async () => {
const payload = {
{{field1}}: '{{value1}}',
{{field2}}: '{{value2}}',
};
const response = await request(app)
.post('{{path}}')
.set('Authorization', `Bearer ${authToken}`)
.send(payload)
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
...payload,
createdAt: expect.any(String),
});
// Verify persistence
const dbRecord = await db.{{table}}.findById(response.body.id);
expect(dbRecord).toBeDefined();
});
it('should return 400 for invalid data', async () => {
const invalidPayload = { {{field1}}: '' };
const response = await request(app)
.post('{{path}}')
.set('Authorization', `Bearer ${authToken}`)
.send(invalidPayload)
.expect(400);
expect(response.body).toMatchObject({
error: expect.objectContaining({
code: 'VALIDATION_ERROR',
details: expect.any(Array),
}),
});
});
});
describe('PUT {{path}}/:id', () => {
it('should update existing {{resource}}', async () => {
const existing = await db.{{table}}.create(testFixture);
const updates = { {{field}}: '{{newValue}}' };
const response = await request(app)
.put(`{{path}}/${existing.id}`)
.set('Authorization', `Bearer ${authToken}`)
.send(updates)
.expect(200);
expect(response.body.{{field}}).toBe('{{newValue}}');
});
it('should return 404 for non-existent id', async () => {
await request(app)
.put('{{path}}/non-existent-id')
.set('Authorization', `Bearer ${authToken}`)
.send({ {{field}}: 'value' })
.expect(404);
});
});
describe('DELETE {{path}}/:id', () => {
it('should delete existing {{resource}}', async () => {
const existing = await db.{{table}}.create(testFixture);
await request(app)
.delete(`{{path}}/${existing.id}`)
.set('Authorization', `Bearer ${authToken}`)
.expect(204);
// Verify deletion
const deleted = await db.{{table}}.findById(existing.id);
expect(deleted).toBeNull();
});
});
});
describe('{{Entity}}Repository Integration', () => {
let repository: {{Entity}}Repository;
let db: TestDatabase;
beforeAll(async () => {
db = await createTestDatabase();
repository = new {{Entity}}Repository(db.connection);
});
afterAll(async () => {
await db.close();
});
beforeEach(async () => {
await db.clear('{{table}}');
});
describe('create', () => {
it('should persist entity to database', async () => {
const data: Create{{Entity}}Dto = {
{{field1}}: '{{value1}}',
{{field2}}: {{value2}},
};
const result = await repository.create(data);
expect(result.id).toBeDefined();
expect(result.{{field1}}).toBe(data.{{field1}});
// Verify raw database state
const raw = await db.query(
'SELECT * FROM {{table}} WHERE id = $1',
[result.id]
);
expect(raw.rows[0]).toBeDefined();
});
it('should enforce unique constraints', async () => {
const data = { email: 'test@example.com' };
await repository.create(data);
await expect(repository.create(data))
.rejects.toThrow(/unique constraint/i);
});
});
describe('findById', () => {
it('should return entity when exists', async () => {
const created = await repository.create(testData);
const found = await repository.findById(created.id);
expect(found).toMatchObject(created);
});
it('should return null when not exists', async () => {
const found = await repository.findById('non-existent');
expect(found).toBeNull();
});
});
describe('complex queries', () => {
beforeEach(async () => {
// Seed test data
await Promise.all([
repository.create({ status: 'active', createdAt: new Date('2025-01-01') }),
repository.create({ status: 'active', createdAt: new Date('2025-01-10') }),
repository.create({ status: 'inactive', createdAt: new Date('2025-01-05') }),
]);
});
it('should filter by status', async () => {
const results = await repository.findAll({ status: 'active' });
expect(results).toHaveLength(2);
results.forEach(r => expect(r.status).toBe('active'));
});
it('should sort by date', async () => {
const results = await repository.findAll({
orderBy: 'createdAt',
order: 'desc'
});
expect(results[0].createdAt.getTime())
.toBeGreaterThan(results[1].createdAt.getTime());
});
});
});
describe('{{Service}} Integration', () => {
let service: {{Service}};
let db: TestDatabase;
beforeAll(async () => {
db = await createTestDatabase();
const repository = new {{Repository}}(db.connection);
const externalClient = new MockExternalClient();
service = new {{Service}}(repository, externalClient);
});
afterAll(async () => {
await db.close();
});
beforeEach(async () => {
await db.clear();
});
describe('{{complexOperation}}', () => {
it('should coordinate multiple operations', async () => {
// Arrange
const input = createTestInput();
// Act
const result = await service.{{complexOperation}}(input);
// Assert - verify all side effects
expect(result).toMatchObject(expectedResult);
// Verify database state
const dbState = await db.{{table}}.findById(result.id);
expect(dbState.status).toBe('processed');
// Verify related entities
const related = await db.{{relatedTable}}.findByParentId(result.id);
expect(related).toHaveLength(expectedCount);
});
it('should rollback on failure', async () => {
// Arrange - setup to fail midway
const input = createFailingInput();
// Act & Assert
await expect(service.{{complexOperation}}(input))
.rejects.toThrow();
// Verify rollback
const dbState = await db.{{table}}.findAll();
expect(dbState).toHaveLength(0);
});
});
});
import { setupServer } from 'msw/node';
import { rest } from 'msw';
const mockServer = setupServer(
rest.get('https://api.external.com/resource', (req, res, ctx) => {
return res(ctx.json({ data: 'mocked' }));
}),
rest.post('https://api.external.com/resource', (req, res, ctx) => {
return res(ctx.status(201), ctx.json({ id: 'new-id' }));
})
);
describe('ExternalService Integration', () => {
beforeAll(() => mockServer.listen());
afterEach(() => mockServer.resetHandlers());
afterAll(() => mockServer.close());
it('should handle external API response', async () => {
const service = new ExternalService();
const result = await service.fetchResource();
expect(result.data).toBe('mocked');
});
it('should handle external API errors', async () => {
mockServer.use(
rest.get('https://api.external.com/resource', (req, res, ctx) => {
return res(ctx.status(500));
})
);
const service = new ExternalService();
await expect(service.fetchResource())
.rejects.toThrow('External service unavailable');
});
});
describe('PaymentService Contract', () => {
it('should match expected request format', async () => {
let capturedRequest: any;
mockServer.use(
rest.post('https://payment.api/charge', async (req, res, ctx) => {
capturedRequest = await req.json();
return res(ctx.json({ success: true }));
})
);
await paymentService.charge({
amount: 1000,
currency: 'USD',
customerId: 'cust_123',
});
expect(capturedRequest).toMatchObject({
amount_cents: 1000,
currency: 'USD',
customer_id: 'cust_123',
idempotency_key: expect.any(String),
});
});
});
// Shared test database setup
export async function createTestDatabase(): Promise<TestDatabase> {
const connection = await createConnection({
...testConfig,
database: `test_${process.env.JEST_WORKER_ID}`,
});
await runMigrations(connection);
return {
connection,
clear: async (table?: string) => {
if (table) {
await connection.query(`TRUNCATE ${table} CASCADE`);
} else {
await connection.query('TRUNCATE ALL TABLES CASCADE');
}
},
seed: async (fixtures: Fixtures) => {
for (const [table, records] of Object.entries(fixtures)) {
await connection.insert(table, records);
}
},
close: async () => {
await connection.close();
},
};
}
// fixtures/users.ts
export const userFixtures = {
admin: {
id: 'user-admin',
email: 'admin@test.com',
role: 'admin',
},
regular: {
id: 'user-regular',
email: 'user@test.com',
role: 'user',
},
};
// Usage in tests
beforeEach(async () => {
await db.seed({ users: Object.values(userFixtures) });
});
// jest.config.js
module.exports = {
maxWorkers: 4,
// Each worker gets isolated database
globalSetup: './test/setup.ts',
globalTeardown: './test/teardown.ts',
};
// Reuse connections across tests
let sharedPool: Pool;
beforeAll(async () => {
sharedPool = await createPool(testConfig);
});
afterAll(async () => {
await sharedPool.end();
});
| Agent | Integration Testing Use |
|---|---|
| QA Engineer | Primary: Writes integration tests BEFORE implementation (TDD Red phase), executes at QA gate |
| Senior Developer | Reviews integration tests, guides on test scope |
| DevOps Engineer | CI/CD integration test execution, enforces 100% pass rate |
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.