Initial commit
This commit is contained in:
14
.claude-plugin/plugin.json
Normal file
14
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "testing-qa-suite",
|
||||||
|
"description": "Comprehensive testing and quality assurance patterns across languages and frameworks",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Brock"
|
||||||
|
},
|
||||||
|
"agents": [
|
||||||
|
"./agents"
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
"./commands"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# testing-qa-suite
|
||||||
|
|
||||||
|
Comprehensive testing and quality assurance patterns across languages and frameworks
|
||||||
584
agents/test-suite-builder.md
Normal file
584
agents/test-suite-builder.md
Normal file
@@ -0,0 +1,584 @@
|
|||||||
|
# Test Suite Builder Agent
|
||||||
|
|
||||||
|
You are an autonomous agent specialized in building comprehensive test suites and implementing quality assurance practices across different programming languages and frameworks.
|
||||||
|
|
||||||
|
## Your Mission
|
||||||
|
|
||||||
|
Build production-ready test suites that ensure code quality, catch regressions, and provide confidence for refactoring and deployment.
|
||||||
|
|
||||||
|
## Core Responsibilities
|
||||||
|
|
||||||
|
### 1. Analyze Codebase for Testing Needs
|
||||||
|
- Identify untested or under-tested code paths
|
||||||
|
- Assess current test coverage and quality
|
||||||
|
- Determine appropriate testing strategies (unit, integration, E2E)
|
||||||
|
- Identify critical paths that need higher coverage
|
||||||
|
- Review existing test infrastructure and tooling
|
||||||
|
|
||||||
|
### 2. Design Test Strategy
|
||||||
|
- Choose appropriate testing frameworks for the language/stack
|
||||||
|
- Define testing pyramid: unit (70%), integration (20%), E2E (10%)
|
||||||
|
- Establish coverage targets based on code criticality
|
||||||
|
- Plan test data management and fixtures
|
||||||
|
- Design mocking strategy for external dependencies
|
||||||
|
|
||||||
|
### 3. Implement Test Infrastructure
|
||||||
|
- Set up testing frameworks (Jest, pytest, Go testing, etc.)
|
||||||
|
- Configure test runners and coverage tools
|
||||||
|
- Implement test data builders and factories
|
||||||
|
- Set up database fixtures for integration tests
|
||||||
|
- Configure CI/CD integration for automated testing
|
||||||
|
|
||||||
|
### 4. Write Comprehensive Tests
|
||||||
|
|
||||||
|
#### Unit Tests
|
||||||
|
```typescript
|
||||||
|
// Example: TypeScript/Jest unit test with proper structure
|
||||||
|
describe('OrderService', () => {
|
||||||
|
let service: OrderService;
|
||||||
|
let mockPaymentGateway: jest.Mocked<PaymentGateway>;
|
||||||
|
let mockInventory: jest.Mocked<InventoryService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPaymentGateway = {
|
||||||
|
charge: jest.fn(),
|
||||||
|
refund: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
mockInventory = {
|
||||||
|
reserve: jest.fn(),
|
||||||
|
release: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
service = new OrderService(mockPaymentGateway, mockInventory);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createOrder', () => {
|
||||||
|
it('should process order successfully with valid input', async () => {
|
||||||
|
// Arrange
|
||||||
|
const orderData = {
|
||||||
|
items: [{ id: 1, quantity: 2, price: 10.00 }],
|
||||||
|
customerId: 'cust_123',
|
||||||
|
};
|
||||||
|
mockInventory.reserve.mockResolvedValue(true);
|
||||||
|
mockPaymentGateway.charge.mockResolvedValue({ id: 'ch_123', status: 'succeeded' });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await service.createOrder(orderData);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.status).toBe('completed');
|
||||||
|
expect(mockInventory.reserve).toHaveBeenCalledWith(orderData.items);
|
||||||
|
expect(mockPaymentGateway.charge).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ amount: 20.00 })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should rollback inventory if payment fails', async () => {
|
||||||
|
// Arrange
|
||||||
|
const orderData = { items: [{ id: 1, quantity: 2 }], customerId: 'cust_123' };
|
||||||
|
mockInventory.reserve.mockResolvedValue(true);
|
||||||
|
mockPaymentGateway.charge.mockRejectedValue(new Error('Payment failed'));
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(service.createOrder(orderData)).rejects.toThrow('Payment failed');
|
||||||
|
expect(mockInventory.release).toHaveBeenCalledWith(orderData.items);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate order before processing', async () => {
|
||||||
|
// Arrange
|
||||||
|
const invalidOrder = { items: [], customerId: 'cust_123' };
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(service.createOrder(invalidOrder)).rejects.toThrow('Order must contain items');
|
||||||
|
expect(mockInventory.reserve).not.toHaveBeenCalled();
|
||||||
|
expect(mockPaymentGateway.charge).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Integration Tests
|
||||||
|
```python
|
||||||
|
# Example: Python/pytest integration test with database
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from myapp.models import User, Order, Base
|
||||||
|
from myapp.repositories import OrderRepository
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def db_session():
|
||||||
|
"""Create test database and session for each test."""
|
||||||
|
engine = create_engine('sqlite:///:memory:')
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
Session = sessionmaker(bind=engine)
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
yield session
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_user(db_session):
|
||||||
|
"""Create a sample user for testing."""
|
||||||
|
user = User(email='test@example.com', name='Test User')
|
||||||
|
db_session.add(user)
|
||||||
|
db_session.commit()
|
||||||
|
return user
|
||||||
|
|
||||||
|
class TestOrderRepository:
|
||||||
|
def test_create_order_with_items(self, db_session, sample_user):
|
||||||
|
"""Test creating order with items persists correctly."""
|
||||||
|
# Arrange
|
||||||
|
repo = OrderRepository(db_session)
|
||||||
|
order_data = {
|
||||||
|
'user_id': sample_user.id,
|
||||||
|
'items': [
|
||||||
|
{'product_id': 1, 'quantity': 2, 'price': 10.00},
|
||||||
|
{'product_id': 2, 'quantity': 1, 'price': 25.00},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Act
|
||||||
|
order = repo.create_order(order_data)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
retrieved_order = repo.get_by_id(order.id)
|
||||||
|
assert retrieved_order is not None
|
||||||
|
assert len(retrieved_order.items) == 2
|
||||||
|
assert retrieved_order.total_amount == 45.00
|
||||||
|
assert retrieved_order.user_id == sample_user.id
|
||||||
|
|
||||||
|
def test_find_orders_by_user(self, db_session, sample_user):
|
||||||
|
"""Test querying orders by user returns correct results."""
|
||||||
|
# Arrange
|
||||||
|
repo = OrderRepository(db_session)
|
||||||
|
order1 = repo.create_order({'user_id': sample_user.id, 'items': []})
|
||||||
|
order2 = repo.create_order({'user_id': sample_user.id, 'items': []})
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
# Act
|
||||||
|
user_orders = repo.find_by_user_id(sample_user.id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert len(user_orders) == 2
|
||||||
|
assert all(order.user_id == sample_user.id for order in user_orders)
|
||||||
|
|
||||||
|
def test_update_order_status(self, db_session, sample_user):
|
||||||
|
"""Test updating order status reflects in database."""
|
||||||
|
# Arrange
|
||||||
|
repo = OrderRepository(db_session)
|
||||||
|
order = repo.create_order({'user_id': sample_user.id, 'items': []})
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
# Act
|
||||||
|
repo.update_status(order.id, 'shipped')
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
updated_order = repo.get_by_id(order.id)
|
||||||
|
assert updated_order.status == 'shipped'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### End-to-End Tests
|
||||||
|
```typescript
|
||||||
|
// Example: Playwright E2E test with Page Object Model
|
||||||
|
import { test, expect, Page } from '@playwright/test';
|
||||||
|
|
||||||
|
class CheckoutPage {
|
||||||
|
constructor(private page: Page) {}
|
||||||
|
|
||||||
|
async addToCart(productId: string) {
|
||||||
|
await this.page.goto(`/products/${productId}`);
|
||||||
|
await this.page.click('[data-testid="add-to-cart"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
async proceedToCheckout() {
|
||||||
|
await this.page.click('[data-testid="cart-icon"]');
|
||||||
|
await this.page.click('[data-testid="checkout-button"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
async fillShippingInfo(info: ShippingInfo) {
|
||||||
|
await this.page.fill('[name="address"]', info.address);
|
||||||
|
await this.page.fill('[name="city"]', info.city);
|
||||||
|
await this.page.fill('[name="zip"]', info.zip);
|
||||||
|
await this.page.click('[data-testid="continue-to-payment"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
async fillPaymentInfo(card: CardInfo) {
|
||||||
|
await this.page.fill('[name="cardNumber"]', card.number);
|
||||||
|
await this.page.fill('[name="expiry"]', card.expiry);
|
||||||
|
await this.page.fill('[name="cvc"]', card.cvc);
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitOrder() {
|
||||||
|
await this.page.click('[data-testid="place-order"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrderConfirmation() {
|
||||||
|
await this.page.waitForSelector('[data-testid="order-confirmation"]');
|
||||||
|
return this.page.locator('[data-testid="order-number"]').textContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('Checkout Flow', () => {
|
||||||
|
let checkoutPage: CheckoutPage;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
checkoutPage = new CheckoutPage(page);
|
||||||
|
|
||||||
|
// Login before each test
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.fill('[name="email"]', 'test@example.com');
|
||||||
|
await page.fill('[name="password"]', 'password123');
|
||||||
|
await page.click('[data-testid="login-button"]');
|
||||||
|
await page.waitForURL('/dashboard');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('complete purchase flow with credit card', async ({ page }) => {
|
||||||
|
// Add products to cart
|
||||||
|
await checkoutPage.addToCart('prod_123');
|
||||||
|
await checkoutPage.addToCart('prod_456');
|
||||||
|
|
||||||
|
// Proceed to checkout
|
||||||
|
await checkoutPage.proceedToCheckout();
|
||||||
|
|
||||||
|
// Fill shipping information
|
||||||
|
await checkoutPage.fillShippingInfo({
|
||||||
|
address: '123 Main St',
|
||||||
|
city: 'New York',
|
||||||
|
zip: '10001',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fill payment information
|
||||||
|
await checkoutPage.fillPaymentInfo({
|
||||||
|
number: '4242424242424242',
|
||||||
|
expiry: '12/25',
|
||||||
|
cvc: '123',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Submit order
|
||||||
|
await checkoutPage.submitOrder();
|
||||||
|
|
||||||
|
// Verify confirmation
|
||||||
|
const orderNumber = await checkoutPage.getOrderConfirmation();
|
||||||
|
expect(orderNumber).toMatch(/^ORD-\d+$/);
|
||||||
|
|
||||||
|
// Verify email sent (could check email service or database)
|
||||||
|
// Verify inventory updated (could check API or database)
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle payment failure gracefully', async ({ page }) => {
|
||||||
|
// Intercept payment API to simulate failure
|
||||||
|
await page.route('**/api/payments', route =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 402,
|
||||||
|
body: JSON.stringify({ error: 'Card declined' }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await checkoutPage.addToCart('prod_123');
|
||||||
|
await checkoutPage.proceedToCheckout();
|
||||||
|
|
||||||
|
await checkoutPage.fillShippingInfo({
|
||||||
|
address: '123 Main St',
|
||||||
|
city: 'New York',
|
||||||
|
zip: '10001',
|
||||||
|
});
|
||||||
|
|
||||||
|
await checkoutPage.fillPaymentInfo({
|
||||||
|
number: '4000000000000002', // Test card that declines
|
||||||
|
expiry: '12/25',
|
||||||
|
cvc: '123',
|
||||||
|
});
|
||||||
|
|
||||||
|
await checkoutPage.submitOrder();
|
||||||
|
|
||||||
|
// Verify error message displayed
|
||||||
|
await expect(page.locator('[data-testid="payment-error"]'))
|
||||||
|
.toContainText('Card declined');
|
||||||
|
|
||||||
|
// Verify user can retry
|
||||||
|
await expect(page.locator('[data-testid="place-order"]'))
|
||||||
|
.toBeEnabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Implement Test Data Management
|
||||||
|
|
||||||
|
#### Test Builders Pattern
|
||||||
|
```typescript
|
||||||
|
class UserBuilder {
|
||||||
|
private user: Partial<User> = {
|
||||||
|
email: `test-${Date.now()}@example.com`,
|
||||||
|
name: 'Test User',
|
||||||
|
role: 'user',
|
||||||
|
verified: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
withEmail(email: string): this {
|
||||||
|
this.user.email = email;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
withRole(role: string): this {
|
||||||
|
this.user.role = role;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
unverified(): this {
|
||||||
|
this.user.verified = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
build(): User {
|
||||||
|
return this.user as User;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(db: Database): Promise<User> {
|
||||||
|
return db.users.create(this.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const adminUser = await new UserBuilder()
|
||||||
|
.withEmail('admin@example.com')
|
||||||
|
.withRole('admin')
|
||||||
|
.create(db);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Configure Coverage and Quality Gates
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// jest.config.js
|
||||||
|
module.exports = {
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'src/**/*.{js,ts}',
|
||||||
|
'!src/**/*.test.{js,ts}',
|
||||||
|
'!src/**/*.spec.{js,ts}',
|
||||||
|
'!src/**/index.{js,ts}',
|
||||||
|
],
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
branches: 75,
|
||||||
|
functions: 75,
|
||||||
|
lines: 80,
|
||||||
|
statements: 80,
|
||||||
|
},
|
||||||
|
// Higher requirements for critical modules
|
||||||
|
'./src/payment/**/*.ts': {
|
||||||
|
branches: 90,
|
||||||
|
functions: 90,
|
||||||
|
lines: 95,
|
||||||
|
statements: 95,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
coverageReporters: ['text', 'lcov', 'html', 'json-summary'],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Mock External Dependencies Properly
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Use MSW for HTTP mocking
|
||||||
|
import { rest } from 'msw';
|
||||||
|
import { setupServer } from 'msw/node';
|
||||||
|
|
||||||
|
const handlers = [
|
||||||
|
rest.post('https://api.stripe.com/v1/charges', (req, res, ctx) => {
|
||||||
|
return res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({
|
||||||
|
id: 'ch_test_123',
|
||||||
|
status: 'succeeded',
|
||||||
|
amount: 1000,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
rest.get('https://api.github.com/users/:username', (req, res, ctx) => {
|
||||||
|
return res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({
|
||||||
|
login: req.params.username,
|
||||||
|
name: 'Test User',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const server = setupServer(...handlers);
|
||||||
|
|
||||||
|
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
|
||||||
|
afterEach(() => server.resetHandlers());
|
||||||
|
afterAll(() => server.close());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Best Practices You Must Follow
|
||||||
|
|
||||||
|
### 1. Arrange-Act-Assert Pattern
|
||||||
|
Always structure tests with clear sections
|
||||||
|
|
||||||
|
### 2. Test Behavior, Not Implementation
|
||||||
|
Focus on what the code does, not how it does it
|
||||||
|
|
||||||
|
### 3. One Assertion Per Test (When Possible)
|
||||||
|
Makes failures easier to diagnose
|
||||||
|
|
||||||
|
### 4. Use Descriptive Test Names
|
||||||
|
```typescript
|
||||||
|
// Good
|
||||||
|
test('should send confirmation email after successful order', () => {});
|
||||||
|
test('should rollback transaction when payment fails', () => {});
|
||||||
|
|
||||||
|
// Bad
|
||||||
|
test('test order', () => {});
|
||||||
|
test('it works', () => {});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Keep Tests Independent
|
||||||
|
No test should depend on another test's execution or state
|
||||||
|
|
||||||
|
### 6. Use Test Doubles Appropriately
|
||||||
|
- **Stub**: Provides canned answers to calls
|
||||||
|
- **Mock**: Expects specific calls with specific arguments
|
||||||
|
- **Spy**: Records how it was called
|
||||||
|
- **Fake**: Working implementation, but simplified
|
||||||
|
|
||||||
|
### 7. Test Edge Cases and Error Conditions
|
||||||
|
```typescript
|
||||||
|
describe('divide', () => {
|
||||||
|
it('should divide positive numbers', () => {});
|
||||||
|
it('should handle negative numbers', () => {});
|
||||||
|
it('should throw error when dividing by zero', () => {});
|
||||||
|
it('should handle floating point precision', () => {});
|
||||||
|
it('should handle very large numbers', () => {});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Avoid Test Interdependence
|
||||||
|
```typescript
|
||||||
|
// Bad
|
||||||
|
let userId: number;
|
||||||
|
test('creates user', () => { userId = createUser(); });
|
||||||
|
test('updates user', () => { updateUser(userId); }); // Depends on previous test
|
||||||
|
|
||||||
|
// Good
|
||||||
|
test('updates user', () => {
|
||||||
|
const userId = createUser();
|
||||||
|
updateUser(userId);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Framework-Specific Patterns
|
||||||
|
|
||||||
|
### TypeScript/Jest
|
||||||
|
```typescript
|
||||||
|
// Setup and teardown
|
||||||
|
beforeAll(() => {/* runs once before all tests */});
|
||||||
|
beforeEach(() => {/* runs before each test */});
|
||||||
|
afterEach(() => {/* runs after each test */});
|
||||||
|
afterAll(() => {/* runs once after all tests */});
|
||||||
|
|
||||||
|
// Async testing
|
||||||
|
test('async operation', async () => {
|
||||||
|
await expect(asyncFunction()).resolves.toBe(true);
|
||||||
|
await expect(failingAsync()).rejects.toThrow('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mocking modules
|
||||||
|
jest.mock('./userService', () => ({
|
||||||
|
getUser: jest.fn().mockResolvedValue({ id: 1, name: 'Test' }),
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python/pytest
|
||||||
|
```python
|
||||||
|
# Fixtures
|
||||||
|
@pytest.fixture
|
||||||
|
def db():
|
||||||
|
connection = create_connection()
|
||||||
|
yield connection
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
# Parametrized tests
|
||||||
|
@pytest.mark.parametrize("input,expected", [
|
||||||
|
(1, 2),
|
||||||
|
(2, 4),
|
||||||
|
(3, 6),
|
||||||
|
])
|
||||||
|
def test_double(input, expected):
|
||||||
|
assert double(input) == expected
|
||||||
|
|
||||||
|
# Async tests
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_async_function():
|
||||||
|
result = await async_function()
|
||||||
|
assert result == expected
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
```go
|
||||||
|
// Table-driven tests
|
||||||
|
func TestAdd(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
a, b int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"positive numbers", 2, 3, 5},
|
||||||
|
{"negative numbers", -1, -1, -2},
|
||||||
|
{"mixed", -1, 1, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := Add(tt.a, tt.b)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deliverables
|
||||||
|
|
||||||
|
When building a test suite, provide:
|
||||||
|
|
||||||
|
1. **Test Infrastructure Setup**
|
||||||
|
- Configuration files for test frameworks
|
||||||
|
- CI/CD integration scripts
|
||||||
|
- Coverage reporting setup
|
||||||
|
|
||||||
|
2. **Comprehensive Test Suite**
|
||||||
|
- Unit tests for business logic
|
||||||
|
- Integration tests for database and external services
|
||||||
|
- E2E tests for critical user flows
|
||||||
|
|
||||||
|
3. **Test Utilities**
|
||||||
|
- Test data builders and factories
|
||||||
|
- Custom matchers and assertions
|
||||||
|
- Helper functions for common test scenarios
|
||||||
|
|
||||||
|
4. **Documentation**
|
||||||
|
- Testing strategy and conventions
|
||||||
|
- How to run tests locally and in CI
|
||||||
|
- Coverage requirements and quality gates
|
||||||
|
|
||||||
|
5. **Quality Reports**
|
||||||
|
- Current coverage metrics
|
||||||
|
- Identified gaps and recommendations
|
||||||
|
- Performance benchmarks for test suite
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- Test coverage meets or exceeds defined thresholds
|
||||||
|
- All critical paths have comprehensive test coverage
|
||||||
|
- Tests are fast, reliable, and maintainable
|
||||||
|
- CI/CD pipeline includes automated testing
|
||||||
|
- Team can confidently refactor with test safety net
|
||||||
|
- Test failures provide clear, actionable feedback
|
||||||
720
commands/testing-patterns.md
Normal file
720
commands/testing-patterns.md
Normal file
@@ -0,0 +1,720 @@
|
|||||||
|
# Testing & Quality Assurance Patterns
|
||||||
|
|
||||||
|
Comprehensive testing strategies and patterns across languages and frameworks.
|
||||||
|
|
||||||
|
## Unit Testing Patterns
|
||||||
|
|
||||||
|
### Test Structure: Arrange-Act-Assert (AAA)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// TypeScript with Jest
|
||||||
|
describe('UserService', () => {
|
||||||
|
it('should create user with valid data', async () => {
|
||||||
|
// Arrange
|
||||||
|
const userData = { email: 'test@example.com', name: 'Test User' };
|
||||||
|
const mockRepository = {
|
||||||
|
save: jest.fn().mockResolvedValue({ id: 1, ...userData })
|
||||||
|
};
|
||||||
|
const service = new UserService(mockRepository);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await service.createUser(userData);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result.id).toBe(1);
|
||||||
|
expect(result.email).toBe(userData.email);
|
||||||
|
expect(mockRepository.save).toHaveBeenCalledWith(userData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python with pytest
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
from myapp.services import UserService
|
||||||
|
from unittest.mock import Mock, AsyncMock
|
||||||
|
|
||||||
|
class TestUserService:
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_repository(self):
|
||||||
|
repo = Mock()
|
||||||
|
repo.save = AsyncMock(return_value={'id': 1, 'email': 'test@example.com'})
|
||||||
|
return repo
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def service(self, mock_repository):
|
||||||
|
return UserService(mock_repository)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_user_with_valid_data(self, service, mock_repository):
|
||||||
|
# Arrange
|
||||||
|
user_data = {'email': 'test@example.com', 'name': 'Test User'}
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = await service.create_user(user_data)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result['id'] == 1
|
||||||
|
assert result['email'] == user_data['email']
|
||||||
|
mock_repository.save.assert_called_once_with(user_data)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go Table-Driven Tests
|
||||||
|
|
||||||
|
```go
|
||||||
|
package user_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockRepository struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockRepository) Save(user *User) error {
|
||||||
|
args := m.Called(user)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserService_CreateUser(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input *User
|
||||||
|
mockSetup func(*MockRepository)
|
||||||
|
wantErr bool
|
||||||
|
errContains string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid user",
|
||||||
|
input: &User{Email: "test@example.com", Name: "Test"},
|
||||||
|
mockSetup: func(m *MockRepository) {
|
||||||
|
m.On("Save", mock.Anything).Return(nil)
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid email",
|
||||||
|
input: &User{Email: "invalid", Name: "Test"},
|
||||||
|
mockSetup: func(m *MockRepository) {},
|
||||||
|
wantErr: true,
|
||||||
|
errContains: "invalid email",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
mockRepo := new(MockRepository)
|
||||||
|
tt.mockSetup(mockRepo)
|
||||||
|
service := NewUserService(mockRepo)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := service.CreateUser(tt.input)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), tt.errContains)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
mockRepo.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rust with rstest
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use rstest::*;
|
||||||
|
use mockall::predicate::*;
|
||||||
|
use mockall::mock;
|
||||||
|
|
||||||
|
mock! {
|
||||||
|
Repository {}
|
||||||
|
impl UserRepository for Repository {
|
||||||
|
fn save(&self, user: &User) -> Result<User, Error>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fixture]
|
||||||
|
fn user_data() -> UserData {
|
||||||
|
UserData {
|
||||||
|
email: "test@example.com".to_string(),
|
||||||
|
name: "Test User".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_create_user_success(user_data: UserData) {
|
||||||
|
// Arrange
|
||||||
|
let mut mock_repo = MockRepository::new();
|
||||||
|
mock_repo
|
||||||
|
.expect_save()
|
||||||
|
.times(1)
|
||||||
|
.returning(|user| Ok(user.clone()));
|
||||||
|
|
||||||
|
let service = UserService::new(mock_repo);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let result = service.create_user(user_data.clone());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let user = result.unwrap();
|
||||||
|
assert_eq!(user.email, user_data.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("invalid-email", "Test", "invalid email")]
|
||||||
|
#[case("test@example.com", "", "name required")]
|
||||||
|
fn test_create_user_validation_errors(
|
||||||
|
#[case] email: &str,
|
||||||
|
#[case] name: &str,
|
||||||
|
#[case] expected_error: &str
|
||||||
|
) {
|
||||||
|
let mock_repo = MockRepository::new();
|
||||||
|
let service = UserService::new(mock_repo);
|
||||||
|
|
||||||
|
let user_data = UserData {
|
||||||
|
email: email.to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = service.create_user(user_data);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().to_string().contains(expected_error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Data Builders Pattern
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class UserBuilder {
|
||||||
|
private user: Partial<User> = {
|
||||||
|
email: 'test@example.com',
|
||||||
|
name: 'Test User',
|
||||||
|
role: 'user',
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
withEmail(email: string): this {
|
||||||
|
this.user.email = email;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
withRole(role: string): this {
|
||||||
|
this.user.role = role;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inactive(): this {
|
||||||
|
this.user.active = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
build(): User {
|
||||||
|
return this.user as User;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in tests
|
||||||
|
const adminUser = new UserBuilder()
|
||||||
|
.withEmail('admin@example.com')
|
||||||
|
.withRole('admin')
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const inactiveUser = new UserBuilder()
|
||||||
|
.inactive()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python Factory Boy
|
||||||
|
|
||||||
|
```python
|
||||||
|
import factory
|
||||||
|
from factory import fuzzy
|
||||||
|
from myapp.models import User, Post
|
||||||
|
|
||||||
|
class UserFactory(factory.Factory):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
|
||||||
|
email = factory.Sequence(lambda n: f'user{n}@example.com')
|
||||||
|
name = factory.Faker('name')
|
||||||
|
role = 'user'
|
||||||
|
active = True
|
||||||
|
|
||||||
|
class AdminUserFactory(UserFactory):
|
||||||
|
role = 'admin'
|
||||||
|
|
||||||
|
class PostFactory(factory.Factory):
|
||||||
|
class Meta:
|
||||||
|
model = Post
|
||||||
|
|
||||||
|
title = factory.Faker('sentence')
|
||||||
|
content = factory.Faker('text')
|
||||||
|
author = factory.SubFactory(UserFactory)
|
||||||
|
published_at = factory.Faker('date_time_this_year')
|
||||||
|
|
||||||
|
# Usage in tests
|
||||||
|
def test_user_can_create_post():
|
||||||
|
user = UserFactory()
|
||||||
|
post = PostFactory(author=user)
|
||||||
|
assert post.author.id == user.id
|
||||||
|
|
||||||
|
def test_admin_has_permissions():
|
||||||
|
admin = AdminUserFactory()
|
||||||
|
assert admin.role == 'admin'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Testing Patterns
|
||||||
|
|
||||||
|
### Database Integration Tests (TypeScript)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
|
describe('UserRepository Integration Tests', () => {
|
||||||
|
let dataSource: DataSource;
|
||||||
|
let repository: UserRepository;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Setup test database
|
||||||
|
dataSource = new DataSource({
|
||||||
|
type: 'postgres',
|
||||||
|
host: 'localhost',
|
||||||
|
port: 5433, // Test DB port
|
||||||
|
database: 'test_db',
|
||||||
|
entities: [User],
|
||||||
|
synchronize: true,
|
||||||
|
});
|
||||||
|
await dataSource.initialize();
|
||||||
|
repository = new UserRepository(dataSource);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await dataSource.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Clean database before each test
|
||||||
|
await dataSource.synchronize(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should persist and retrieve user', async () => {
|
||||||
|
const userData = { email: 'test@example.com', name: 'Test' };
|
||||||
|
|
||||||
|
const saved = await repository.save(userData);
|
||||||
|
expect(saved.id).toBeDefined();
|
||||||
|
|
||||||
|
const retrieved = await repository.findById(saved.id);
|
||||||
|
expect(retrieved?.email).toBe(userData.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enforce unique email constraint', async () => {
|
||||||
|
const userData = { email: 'unique@example.com', name: 'Test' };
|
||||||
|
|
||||||
|
await repository.save(userData);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
repository.save(userData)
|
||||||
|
).rejects.toThrow(/duplicate key/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Integration Tests (Python with pytest)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
from httpx import AsyncClient
|
||||||
|
from myapp.main import app
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def client():
|
||||||
|
async with AsyncClient(app=app, base_url="http://test") as ac:
|
||||||
|
yield ac
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def authenticated_client(client):
|
||||||
|
# Login and get token
|
||||||
|
response = await client.post("/auth/login", json={
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "password123"
|
||||||
|
})
|
||||||
|
token = response.json()["access_token"]
|
||||||
|
client.headers["Authorization"] = f"Bearer {token}"
|
||||||
|
return client
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_user_endpoint(client):
|
||||||
|
response = await client.post("/users", json={
|
||||||
|
"email": "new@example.com",
|
||||||
|
"name": "New User"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
data = response.json()
|
||||||
|
assert data["email"] == "new@example.com"
|
||||||
|
assert "id" in data
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_protected_resource(authenticated_client):
|
||||||
|
response = await authenticated_client.get("/users/me")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["email"] == "test@example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
## End-to-End Testing
|
||||||
|
|
||||||
|
### Playwright (TypeScript)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('User Registration Flow', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should register new user successfully', async ({ page }) => {
|
||||||
|
// Navigate to registration
|
||||||
|
await page.click('text=Sign Up');
|
||||||
|
|
||||||
|
// Fill form
|
||||||
|
await page.fill('[data-testid="email-input"]', 'newuser@example.com');
|
||||||
|
await page.fill('[data-testid="password-input"]', 'SecurePass123!');
|
||||||
|
await page.fill('[data-testid="name-input"]', 'New User');
|
||||||
|
|
||||||
|
// Submit
|
||||||
|
await page.click('[data-testid="submit-button"]');
|
||||||
|
|
||||||
|
// Verify success
|
||||||
|
await expect(page.locator('text=Welcome')).toBeVisible();
|
||||||
|
await expect(page).toHaveURL(/\/dashboard/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show validation errors', async ({ page }) => {
|
||||||
|
await page.click('text=Sign Up');
|
||||||
|
|
||||||
|
// Submit empty form
|
||||||
|
await page.click('[data-testid="submit-button"]');
|
||||||
|
|
||||||
|
// Check for error messages
|
||||||
|
await expect(page.locator('text=Email is required')).toBeVisible();
|
||||||
|
await expect(page.locator('text=Password is required')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle server errors gracefully', async ({ page }) => {
|
||||||
|
// Intercept API call and force error
|
||||||
|
await page.route('**/api/auth/register', route =>
|
||||||
|
route.fulfill({ status: 500, body: 'Server Error' })
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.click('text=Sign Up');
|
||||||
|
await page.fill('[data-testid="email-input"]', 'test@example.com');
|
||||||
|
await page.fill('[data-testid="password-input"]', 'password123');
|
||||||
|
await page.click('[data-testid="submit-button"]');
|
||||||
|
|
||||||
|
await expect(page.locator('text=Something went wrong')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Page Object Model pattern
|
||||||
|
class LoginPage {
|
||||||
|
constructor(private page: Page) {}
|
||||||
|
|
||||||
|
async navigate() {
|
||||||
|
await this.page.goto('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(email: string, password: string) {
|
||||||
|
await this.page.fill('[data-testid="email-input"]', email);
|
||||||
|
await this.page.fill('[data-testid="password-input"]', password);
|
||||||
|
await this.page.click('[data-testid="login-button"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getErrorMessage() {
|
||||||
|
return this.page.locator('[data-testid="error-message"]').textContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('login with invalid credentials', async ({ page }) => {
|
||||||
|
const loginPage = new LoginPage(page);
|
||||||
|
await loginPage.navigate();
|
||||||
|
await loginPage.login('wrong@example.com', 'wrongpass');
|
||||||
|
|
||||||
|
const error = await loginPage.getErrorMessage();
|
||||||
|
expect(error).toContain('Invalid credentials');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Coverage and Quality Metrics
|
||||||
|
|
||||||
|
### Jest Coverage Configuration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// jest.config.js
|
||||||
|
module.exports = {
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'src/**/*.{js,ts}',
|
||||||
|
'!src/**/*.d.ts',
|
||||||
|
'!src/**/*.test.{js,ts}',
|
||||||
|
'!src/**/index.{js,ts}',
|
||||||
|
],
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
branches: 80,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
statements: 80,
|
||||||
|
},
|
||||||
|
'./src/critical/**/*.ts': {
|
||||||
|
branches: 95,
|
||||||
|
functions: 95,
|
||||||
|
lines: 95,
|
||||||
|
statements: 95,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
coverageReporters: ['text', 'lcov', 'html'],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### pytest-cov Configuration
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# pytest.ini
|
||||||
|
[tool:pytest]
|
||||||
|
testpaths = tests
|
||||||
|
python_files = test_*.py
|
||||||
|
python_classes = Test*
|
||||||
|
python_functions = test_*
|
||||||
|
addopts =
|
||||||
|
--cov=myapp
|
||||||
|
--cov-report=html
|
||||||
|
--cov-report=term-missing
|
||||||
|
--cov-fail-under=80
|
||||||
|
--cov-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mocking and Stubbing Strategies
|
||||||
|
|
||||||
|
### Mocking External APIs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Using MSW (Mock Service Worker)
|
||||||
|
import { rest } from 'msw';
|
||||||
|
import { setupServer } from 'msw/node';
|
||||||
|
|
||||||
|
const handlers = [
|
||||||
|
rest.get('https://api.github.com/users/:username', (req, res, ctx) => {
|
||||||
|
const { username } = req.params;
|
||||||
|
return res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({
|
||||||
|
login: username,
|
||||||
|
name: 'Test User',
|
||||||
|
public_repos: 42,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
rest.post('https://api.stripe.com/v1/charges', (req, res, ctx) => {
|
||||||
|
return res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({
|
||||||
|
id: 'ch_test_123',
|
||||||
|
status: 'succeeded',
|
||||||
|
amount: 1000,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const server = setupServer(...handlers);
|
||||||
|
|
||||||
|
beforeAll(() => server.listen());
|
||||||
|
afterEach(() => server.resetHandlers());
|
||||||
|
afterAll(() => server.close());
|
||||||
|
|
||||||
|
test('fetches user from GitHub', async () => {
|
||||||
|
const user = await fetchGithubUser('testuser');
|
||||||
|
expect(user.name).toBe('Test User');
|
||||||
|
expect(user.public_repos).toBe(42);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Time Mocking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
test('schedules task for future execution', () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
const callback = jest.fn();
|
||||||
|
|
||||||
|
scheduleTask(callback, 5000);
|
||||||
|
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(5000);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Snapshot Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
|
||||||
|
test('UserCard renders correctly', () => {
|
||||||
|
const user = {
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'john@example.com',
|
||||||
|
avatar: 'https://example.com/avatar.jpg',
|
||||||
|
};
|
||||||
|
|
||||||
|
const tree = renderer.create(<UserCard user={user} />).toJSON();
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inline snapshots
|
||||||
|
test('formats currency correctly', () => {
|
||||||
|
expect(formatCurrency(1234.56)).toMatchInlineSnapshot(`"$1,234.56"`);
|
||||||
|
expect(formatCurrency(0)).toMatchInlineSnapshot(`"$0.00"`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contract Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Pact consumer test
|
||||||
|
import { Pact } from '@pact-foundation/pact';
|
||||||
|
|
||||||
|
const provider = new Pact({
|
||||||
|
consumer: 'FrontendApp',
|
||||||
|
provider: 'UserAPI',
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('User API Contract', () => {
|
||||||
|
beforeAll(() => provider.setup());
|
||||||
|
afterAll(() => provider.finalize());
|
||||||
|
|
||||||
|
test('get user by id', async () => {
|
||||||
|
await provider.addInteraction({
|
||||||
|
state: 'user exists',
|
||||||
|
uponReceiving: 'a request for user',
|
||||||
|
withRequest: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/users/123',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
},
|
||||||
|
willRespondWith: {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: {
|
||||||
|
id: 123,
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'john@example.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = new UserAPI(provider.mockService.baseUrl);
|
||||||
|
const user = await api.getUser(123);
|
||||||
|
|
||||||
|
expect(user.name).toBe('John Doe');
|
||||||
|
await provider.verify();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Test Independence
|
||||||
|
- Each test should be independent and not rely on other tests
|
||||||
|
- Use beforeEach/afterEach for setup and teardown
|
||||||
|
- Avoid shared mutable state
|
||||||
|
|
||||||
|
### 2. Test Naming
|
||||||
|
```typescript
|
||||||
|
// Good: Describes what is being tested and expected outcome
|
||||||
|
test('should return 404 when user does not exist', () => {});
|
||||||
|
test('should send email notification after successful registration', () => {});
|
||||||
|
|
||||||
|
// Bad: Vague or unclear
|
||||||
|
test('test1', () => {});
|
||||||
|
test('it works', () => {});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Avoid Test Interdependence
|
||||||
|
```typescript
|
||||||
|
// Bad
|
||||||
|
let userId: number;
|
||||||
|
|
||||||
|
test('creates user', async () => {
|
||||||
|
userId = await createUser({ name: 'Test' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updates user', async () => {
|
||||||
|
await updateUser(userId, { name: 'Updated' }); // Depends on previous test
|
||||||
|
});
|
||||||
|
|
||||||
|
// Good
|
||||||
|
test('updates user', async () => {
|
||||||
|
const user = await createUser({ name: 'Test' });
|
||||||
|
await updateUser(user.id, { name: 'Updated' });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Test the Interface, Not Implementation
|
||||||
|
```typescript
|
||||||
|
// Bad: Testing implementation details
|
||||||
|
test('should call internal method', () => {
|
||||||
|
const service = new UserService();
|
||||||
|
const spy = jest.spyOn(service as any, '_validateEmail');
|
||||||
|
service.createUser({ email: 'test@example.com' });
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Good: Testing behavior
|
||||||
|
test('should reject invalid email', async () => {
|
||||||
|
const service = new UserService();
|
||||||
|
await expect(
|
||||||
|
service.createUser({ email: 'invalid' })
|
||||||
|
).rejects.toThrow('Invalid email');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Use Fixtures and Factories
|
||||||
|
Keep test data DRY and maintainable
|
||||||
|
|
||||||
|
### 6. Avoid Testing External Dependencies Directly
|
||||||
|
Mock external services and APIs
|
||||||
|
|
||||||
|
### 7. Keep Tests Fast
|
||||||
|
- Use in-memory databases for tests
|
||||||
|
- Mock expensive operations
|
||||||
|
- Run unit tests in parallel
|
||||||
|
- Separate slow integration/E2E tests
|
||||||
|
|
||||||
|
### 8. Measure What Matters
|
||||||
|
- Focus on critical paths
|
||||||
|
- Aim for meaningful coverage, not just high percentages
|
||||||
|
- Test edge cases and error conditions
|
||||||
49
plugin.lock.json
Normal file
49
plugin.lock.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:Dieshen/claude_marketplace:plugins/testing-qa-suite",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "b4eef8bfd22352b6a64b8db5af993b5bc33d87e8",
|
||||||
|
"treeHash": "ab4991f696a7e1d582cde4e9f82eca5454d0621c4eee4220bef3b2988a5f387b",
|
||||||
|
"generatedAt": "2025-11-28T10:10:23.507968Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "testing-qa-suite",
|
||||||
|
"description": "Comprehensive testing and quality assurance patterns across languages and frameworks",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "cf1ca1f7385cc41b9f12bdfcf3580a1a251141ea6f491400dba62636f7501d00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "agents/test-suite-builder.md",
|
||||||
|
"sha256": "db1216396886f55d134ded516ff3b7f57d9aa4b4aa27a6377f76f4fdfdf003bd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "7437e02649b4bb5115200da67bf8221196af3735a384a5720a7850a180a283c6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/testing-patterns.md",
|
||||||
|
"sha256": "ad19b58ccb62ab19bcbe9e3eb8cad9e7b85de3f325becbecdb08b400db0995c5"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "ab4991f696a7e1d582cde4e9f82eca5454d0621c4eee4220bef3b2988a5f387b"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user