技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
quality-integration-testing【数据与研究】Use when the user asks about integration testing, component testing, API testing, database testing, test containers, wiremock, contract testing, or service-level testing. Do NOT use for: unit testing (quality-unit-testing), E2E testing (quality-e2e-testing), or contract testing (quality-contract-testing).
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Design and implement integration tests that verify components work together: API integration, database integration, external service mocking, and containerized infrastructure for realistic environments. This skill covers test architecture, container lifecycle management, data strategies, and CI integration.
User mentions integration testing, component testing, API testing, database testing, TestContainers, WireMock, service-level testing, or inter-component verification.
Integration test suite with containerized infrastructure, service virtualization, and CI configuration.
Structured integration test files with:
Is this dependency your own service/infrastructure?
├── YES → Use real (containerized) for testing
│ ├── Database → TestContainers (PostgreSQL, MySQL, etc.)
│ ├── Message queue → TestContainers (Kafka, RabbitMQ)
│ ├── Cache → TestContainers (Redis)
│ └── Object storage → TestContainers (MinIO)
└── NO → Is it a third-party external service?
├── YES → Simulate with WireMock (contract-based stubs)
└── NO → Is it an infrastructure provider (AWS, GCP)?
├── YES → Use LocalStack or similar emulator
└── NO → Is it a library with side effects?
├── YES → Mock at adapter boundary
└── NO → Real implementation
How many test suites share this service?
├── One suite → Start/stop per suite (beforeAll/afterAll)
└── Multiple suites → Can they share?
├── YES → Singleton container pattern (shared across suites)
└── NO → Start/stop per suite
Is state isolation critical?
├── YES → Fresh container per test class
└── NO → Data cleanup between tests (truncation)
Does CI support Docker?
├── YES → TestContainers with DinD
└── NO → Embedded services or mocked infrastructure
| Aspect | Integration Testing | Unit Testing | E2E Testing |
|---|---|---|---|
| Scope | Component interactions | Single module | Full user workflow |
| Dependencies | Real (containerized) | Mocked | Real (full system) |
| Speed | Seconds to minutes | Milliseconds | Minutes |
| Confidence | High (real infra) | Low (isolated) | Very High |
| Debugging | Medium | Easy | Hard |
| Infrastructure | Docker/containers | None | Full environment |
| When | After unit tests | Every commit | Pre-release |
import pytest
from testcontainers.postgres import PostgresContainer
from sqlalchemy import create_engine, text
@pytest.fixture(scope="module")
def postgres_container():
with PostgresContainer("postgres:16-alpine") as pg:
engine = create_engine(pg.get_connection_url())
with engine.begin() as conn:
conn.execute(text("""
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL
)
"""))
yield pg
@pytest.fixture
def db(postgres_container):
engine = create_engine(postgres_container.get_connection_url())
yield engine
with engine.begin() as conn:
conn.execute(text("TRUNCATE TABLE users RESTART IDENTITY CASCADE"))
class TestUserRepository:
def test_create_user(self, db):
with db.begin() as conn:
result = conn.execute(
text("INSERT INTO users (email, name) VALUES (:email, :name) RETURNING id"),
{"email": "test@example.com", "name": "Test User"},
)
user_id = result.scalar()
assert user_id is not None
def test_find_user_by_email(self, db):
with db.begin() as conn:
conn.execute(
text("INSERT INTO users (email, name) VALUES (:email, :name)"),
{"email": "find@example.com", "name": "Find Me"},
)
with db.begin() as conn:
result = conn.execute(
text("SELECT * FROM users WHERE email = :email"),
{"email": "find@example.com"},
)
user = result.fetchone()
assert user is not None
assert user.name == "Find Me"
def test_email_uniqueness(self, db):
with db.begin() as conn:
conn.execute(
text("INSERT INTO users (email, name) VALUES (:email, :name)"),
{"email": "dup@example.com", "name": "First"},
)
with pytest.raises(Exception):
with db.begin() as conn:
conn.execute(
text("INSERT INTO users (email, name) VALUES (:email, :name)"),
{"email": "dup@example.com", "name": "Second"},
)
@SpringBootTest
@Testcontainers
class OrderServiceIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@Container
static WireMockContainer wiremock = new WireMockContainer("wiremock/wiremock:3.5.4");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("payment.service.url", () -> "http://" + wiremock.getHost() + ":" + wiremock.getMappedPort(8080));
}
@Autowired
private OrderService orderService;
@Test
void shouldCreateOrderWhenPaymentSucceeds() {
stubFor(post(urlEqualTo("/api/payments"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody(""{"id": "pay_123", "status": "confirmed"}"")));
Order order = orderService.placeOrder(customerId, cartItems);
assertThat(order.getStatus()).isEqualTo("CONFIRMED");
assertThat(order.getPaymentId()).isEqualTo("pay_123");
}
}
import { GenericContainer } from "testcontainers";
import { Kafka } from "kafkajs";
describe("Order Event Publisher", () => {
let kafkaContainer: StartedTestContainer;
let kafka: Kafka;
beforeAll(async () => {
kafkaContainer = await new GenericContainer("confluentinc/cp-kafka:7.6.0")
.withExposedPorts(9093)
.withEnvironment({
KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://localhost:9093",
})
.start();
kafka = new Kafka({
clientId: "test",
brokers: [`localhost:${kafkaContainer.getMappedPort(9093)}`],
});
}, 120000);
afterAll(async () => {
await kafkaContainer.stop();
});
it("should publish order created event", async () => {
const producer = kafka.producer();
const consumer = kafka.consumer({ groupId: "test" });
await producer.connect();
await consumer.connect();
await consumer.subscribe({ topic: "order-events" });
const messages: any[] = [];
await consumer.run({
eachMessage: async ({ message }) => {
messages.push(JSON.parse(message.value!.toString()));
},
});
await publisher.publishOrderCreated({ orderId: "ord_123" });
await new Promise((r) => setTimeout(r, 1000));
expect(messages.length).toBe(1);
expect(messages[0].orderId).toBe("ord_123");
});
});
name: Integration Tests
on: pull_request
jobs:
integration:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
matrix:
shard: [1, 2]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- name: Pre-pull container images
run: |
docker pull postgres:16-alpine
docker pull confluentinc/cp-kafka:7.6.0
docker pull wiremock/wiremock:3.5.4
- name: Run integration tests
run: npx vitest run --config vitest.integration.config.ts --shard=${{ matrix.shard }}/2
env:
CI: true
TESTCONTAINERS_RYUK_DISABLED: true
- uses: actions/upload-artifact@v4
if: failure()
with:
name: integration-logs-${{ matrix.shard }}
path: logs/
Using H2 for PostgreSQL or SQLite for MySQL. In-memory substitutes have different SQL dialects, constraint behaviors, and transaction semantics. Tests pass with H2 but fail with PostgreSQL in production. Always use the real database in a container.
Accessing containers before they're ready produces non-deterministic failures. Never use fixed Thread.sleep(). Use predicate-based wait strategies: wait for log message ("database system is ready to accept connections"), HTTP health check, or port listening.
Tests that leave data behind cause failures in subsequent tests. Use truncation between tests (TRUNCATE ... CASCADE). For fully isolated tests, use a fresh container per test class. Never rely on test ordering.
Mocking at the HTTP client level (axios mocks, fetch mocks) instead of the network level. WireMock operates at the HTTP protocol level and validates real request/response contracts. HTTP client mocks are brittle and miss protocol-level issues.
Testing only the happy path (successful database write, successful API response) misses the majority of integration issues. Every happy path must have a corresponding error path: network timeout, HTTP 500, database constraint violation, authentication failure.
Hardcoded ports, hosts, or credentials prevent parallel test execution. Use withExposedPorts for dynamic port mapping. Use @DynamicPropertySource (Spring) or environment variables to inject connection parameters.
| Level | Characteristics | Practices |
|---|---|---|
| 1: Initial | No integration tests | Unit tests only or manual integration testing, no containerized infrastructure |
| 2: Defined | Basic integration tests | TestContainers for database, basic WireMock stubs, sequential execution, some error path testing |
| 3: Managed | Structured integration suite | Containerized databases + message queues + caches, WireMock for all external services, parallel execution, CI integration with pre-pulled images |
| 4: Measured | Comprehensive integration coverage | Error path parity with happy path, migration testing (forward + rollback), performance-sensitive assertions, < 5% flakiness rate |
| 5: Optimized | Integration as release gates | All integration paths covered, chaos-resilient tests (network partition, resource exhaustion), automatic container layer caching, self-healing connection handling |
TESTCONTAINERS_RYUK_DISABLED: true in CI.docker pull in a setup step.After integration testing, hand off to:
quality-e2e-testing — for end-to-end validation of complete workflowsquality-contract-testing — for formal contract verification between servicesquality-regression-testing — for regression suite updatesquality-smoke-testing — for BVT smoke test inclusion` interface EventObserver { onEvent(event: T): Promise; }
class EventBus { private observers: Set<EventObserver> = new Set(); subscribe(observer: EventObserver): void { this.observers.add(observer); } unsubscribe(observer: EventObserver): void { this.observers.delete(observer); } async emit(event: T): Promise { const results = Array.from(this.observers).map(o => o.onEvent(event)); await Promise.allSettled(results); } } `
config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% over 5min | Critical | Page on-call |
| p99 latency | > 2s over 5min | Warning | Investigate |
| Throughput drop | > 50% over 1min | Critical | Check upstream |
| Queue depth | > 1000 over 1min | Warning | Scale consumers |
| Disk usage | > 85% | Warning | Clean or expand |
| Memory usage | > 90% heap | Critical | Restart or scale |
| Anti-Pattern | Symptom | Root Cause | Solution |
|---|---|---|---|
| Premature optimization | Complex code for no measured benefit | Guessing instead of profiling | Measure first, optimize based on data |
| Copy-paste reuse | Duplicate code across codebase | Lack of abstraction | Extract shared logic into libraries |
| Gold-plating | Features with no current requirement | Over-engineering | YAGNI — build what's needed now |
| Magical thinking | Assumptions without validation | Skipping error handling | Handle all failure modes explicitly |
Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge). Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).
| Decision Point | Option A | Option B | Decision Criteria |
|---|---|---|---|
| Dependency type | Internal microservice → Real instance | External API → Mock/stub | Control over service, cost, reliability |
| Database strategy | TestContainers (real DB in container) | In-memory H2/SQLite | Query compatibility, SQL dialect features |
| Message queue | Real broker (TestContainers Kafka) | Mock producer/consumer | Message ordering needs, throughput testing |
| Network complexity | WireMock for HTTP stubs | TestContainer for full service | Need for realistic network behavior |