commit 1a10a48a37ab4d3455a2dbac05fbfc8da03a86e8 Author: Zhongwei Li Date: Sat Nov 29 18:21:49 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..a6ec812 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -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" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..148021f --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# testing-qa-suite + +Comprehensive testing and quality assurance patterns across languages and frameworks diff --git a/agents/test-suite-builder.md b/agents/test-suite-builder.md new file mode 100644 index 0000000..90bcced --- /dev/null +++ b/agents/test-suite-builder.md @@ -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; + let mockInventory: jest.Mocked; + + 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 = { + 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 { + 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 diff --git a/commands/testing-patterns.md b/commands/testing-patterns.md new file mode 100644 index 0000000..03247a1 --- /dev/null +++ b/commands/testing-patterns.md @@ -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; + } + } + + #[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 = { + 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().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 diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..e98afa6 --- /dev/null +++ b/plugin.lock.json @@ -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": [] + } +} \ No newline at end of file