Writes comprehensive test suites including unit tests, integration tests, and API tests to ensure code quality and prevent regressions.
Writes comprehensive unit, integration, and API tests with mocks and coverage analysis.
/plugin marketplace add avovello/cc-plugins/plugin install feature-development@cc-pluginsWrites comprehensive test suites including unit tests, integration tests, and API tests to ensure code quality and prevent regressions.
✅ DOES:
❌ DOES NOT:
/\
/ \ E2E Tests (Few)
/ \ - Full system tests
/------\ - Test critical user flows
/ \
/ Integration Tests (Some)
/ - Test component interactions
/ - Test database, APIs, services
/----------------------------\
Unit Tests (Many)
- Test individual functions
- Fast, isolated, focused
AAA Pattern (Arrange-Act-Assert):
Test Characteristics:
Purpose: Test individual functions/methods in isolation
Example (JavaScript/Jest):
// tests/unit/models/Payment.test.js
const Payment = require('../../../src/models/Payment');
const { ValidationError } = require('../../../src/errors');
describe('Payment Model', () => {
describe('constructor', () => {
it('should create payment with valid data', () => {
// Arrange
const data = {
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'USD',
status: 'pending',
paymentMethod: 'credit_card'
};
// Act
const payment = new Payment(data);
// Assert
expect(payment.id).toBe('123');
expect(payment.orderId).toBe('456');
expect(payment.amount).toBe(99.99);
expect(payment.currency).toBe('USD');
});
it('should set timestamps if not provided', () => {
const data = {
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'USD',
status: 'pending',
paymentMethod: 'credit_card'
};
const payment = new Payment(data);
expect(payment.createdAt).toBeInstanceOf(Date);
expect(payment.updatedAt).toBeInstanceOf(Date);
});
});
describe('validate', () => {
it('should pass validation with valid data', () => {
const payment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'USD',
status: 'pending',
paymentMethod: 'credit_card'
});
expect(() => payment.validate()).not.toThrow();
});
it('should throw error if orderId is missing', () => {
const payment = new Payment({
id: '123',
orderId: '',
userId: '789',
amount: 99.99,
currency: 'USD',
status: 'pending',
paymentMethod: 'credit_card'
});
expect(() => payment.validate()).toThrow(ValidationError);
expect(() => payment.validate()).toThrow('Order ID is required');
});
it('should throw error if amount is negative', () => {
const payment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: -10,
currency: 'USD',
status: 'pending',
paymentMethod: 'credit_card'
});
expect(() => payment.validate()).toThrow(ValidationError);
expect(() => payment.validate()).toThrow('Amount must be positive');
});
it('should throw error if amount is zero', () => {
const payment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 0,
currency: 'USD',
status: 'pending',
paymentMethod: 'credit_card'
});
expect(() => payment.validate()).toThrow('Amount must be positive');
});
it('should throw error for invalid currency', () => {
const payment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'INVALID',
status: 'pending',
paymentMethod: 'credit_card'
});
expect(() => payment.validate()).toThrow('Invalid currency: INVALID');
});
it('should throw error for invalid status', () => {
const payment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'USD',
status: 'invalid_status',
paymentMethod: 'credit_card'
});
expect(() => payment.validate()).toThrow('Invalid status');
});
});
describe('isFinal', () => {
it('should return true for completed status', () => {
const payment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'USD',
status: Payment.Status.COMPLETED,
paymentMethod: 'credit_card'
});
expect(payment.isFinal()).toBe(true);
});
it('should return true for failed status', () => {
const payment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'USD',
status: Payment.Status.FAILED,
paymentMethod: 'credit_card'
});
expect(payment.isFinal()).toBe(true);
});
it('should return false for pending status', () => {
const payment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'USD',
status: Payment.Status.PENDING,
paymentMethod: 'credit_card'
});
expect(payment.isFinal()).toBe(false);
});
});
describe('canRefund', () => {
it('should return true for completed payments', () => {
const payment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'USD',
status: Payment.Status.COMPLETED,
paymentMethod: 'credit_card'
});
expect(payment.canRefund()).toBe(true);
});
it('should return false for pending payments', () => {
const payment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'USD',
status: Payment.Status.PENDING,
paymentMethod: 'credit_card'
});
expect(payment.canRefund()).toBe(false);
});
it('should return false for refunded payments', () => {
const payment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'USD',
status: Payment.Status.REFUNDED,
paymentMethod: 'credit_card'
});
expect(payment.canRefund()).toBe(false);
});
});
describe('toJSON', () => {
it('should return JSON representation', () => {
const now = new Date('2025-01-15T10:30:00Z');
const payment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'USD',
status: 'completed',
paymentMethod: 'credit_card',
gatewayTransactionId: 'txn_abc123',
createdAt: now,
updatedAt: now
});
const json = payment.toJSON();
expect(json).toEqual({
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'USD',
status: 'completed',
paymentMethod: 'credit_card',
gatewayTransactionId: 'txn_abc123',
createdAt: '2025-01-15T10:30:00.000Z',
updatedAt: '2025-01-15T10:30:00.000Z'
});
});
});
});
Example Service Tests with Mocks:
// tests/unit/services/PaymentService.test.js
const PaymentService = require('../../../src/services/PaymentService');
const Payment = require('../../../src/models/Payment');
const { PaymentError, NotFoundError } = require('../../../src/errors');
// Mock dependencies
jest.mock('../../../src/repositories/PaymentRepository');
jest.mock('../../../src/services/PaymentGatewayService');
jest.mock('../../../src/services/OrderService');
jest.mock('../../../src/services/NotificationService');
const PaymentRepository = require('../../../src/repositories/PaymentRepository');
const PaymentGatewayService = require('../../../src/services/PaymentGatewayService');
const OrderService = require('../../../src/services/OrderService');
const NotificationService = require('../../../src/services/NotificationService');
describe('PaymentService', () => {
let paymentService;
let mockPaymentRepository;
let mockPaymentGateway;
let mockOrderService;
let mockNotificationService;
beforeEach(() => {
// Create mock instances
mockPaymentRepository = new PaymentRepository();
mockPaymentGateway = new PaymentGatewayService();
mockOrderService = new OrderService();
mockNotificationService = new NotificationService();
// Inject mocks
paymentService = new PaymentService({
paymentRepository: mockPaymentRepository,
paymentGateway: mockPaymentGateway,
orderService: mockOrderService,
notificationService: mockNotificationService
});
});
afterEach(() => {
jest.clearAllMocks();
});
describe('createPayment', () => {
const validPaymentData = {
orderId: 'order-123',
userId: 'user-456',
amount: 99.99,
currency: 'USD',
paymentMethod: 'credit_card',
paymentDetails: {
cardNumber: '4111111111111111',
expMonth: 12,
expYear: 2025,
cvc: '123'
}
};
it('should create payment successfully', async () => {
// Arrange
mockOrderService.getOrder.mockResolvedValue({
id: 'order-123',
status: 'pending'
});
mockPaymentRepository.create.mockResolvedValue(true);
mockPaymentGateway.processPayment.mockResolvedValue({
transactionId: 'txn_abc123'
});
mockPaymentRepository.update.mockResolvedValue(true);
mockOrderService.markAsPaid.mockResolvedValue(true);
mockNotificationService.sendPaymentConfirmation.mockResolvedValue(true);
// Act
const payment = await paymentService.createPayment(validPaymentData);
// Assert
expect(payment.status).toBe(Payment.Status.COMPLETED);
expect(payment.gatewayTransactionId).toBe('txn_abc123');
expect(mockPaymentRepository.create).toHaveBeenCalledTimes(1);
expect(mockPaymentGateway.processPayment).toHaveBeenCalledTimes(1);
expect(mockPaymentRepository.update).toHaveBeenCalledTimes(1);
expect(mockOrderService.markAsPaid).toHaveBeenCalledWith('order-123');
expect(mockNotificationService.sendPaymentConfirmation).toHaveBeenCalled();
});
it('should throw error if order not found', async () => {
// Arrange
mockOrderService.getOrder.mockResolvedValue(null);
// Act & Assert
await expect(paymentService.createPayment(validPaymentData))
.rejects
.toThrow(PaymentError);
await expect(paymentService.createPayment(validPaymentData))
.rejects
.toThrow('Order not found');
expect(mockPaymentRepository.create).not.toHaveBeenCalled();
});
it('should throw error if order already paid', async () => {
// Arrange
mockOrderService.getOrder.mockResolvedValue({
id: 'order-123',
status: 'paid'
});
// Act & Assert
await expect(paymentService.createPayment(validPaymentData))
.rejects
.toThrow('Order already paid');
});
it('should mark payment as failed if gateway fails', async () => {
// Arrange
mockOrderService.getOrder.mockResolvedValue({
id: 'order-123',
status: 'pending'
});
mockPaymentRepository.create.mockResolvedValue(true);
mockPaymentGateway.processPayment.mockRejectedValue(
new Error('Card declined')
);
mockPaymentRepository.update.mockResolvedValue(true);
// Act & Assert
await expect(paymentService.createPayment(validPaymentData))
.rejects
.toThrow(PaymentError);
expect(mockPaymentRepository.update).toHaveBeenCalledWith(
expect.objectContaining({
status: Payment.Status.FAILED
})
);
expect(mockOrderService.markAsPaid).not.toHaveBeenCalled();
});
});
describe('getPayment', () => {
it('should return payment if found', async () => {
// Arrange
const mockPayment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'USD',
status: 'completed',
paymentMethod: 'credit_card'
});
mockPaymentRepository.findById.mockResolvedValue(mockPayment);
// Act
const payment = await paymentService.getPayment('123');
// Assert
expect(payment.id).toBe('123');
expect(mockPaymentRepository.findById).toHaveBeenCalledWith('123');
});
it('should throw error if payment not found', async () => {
// Arrange
mockPaymentRepository.findById.mockResolvedValue(null);
// Act & Assert
await expect(paymentService.getPayment('999'))
.rejects
.toThrow(NotFoundError);
await expect(paymentService.getPayment('999'))
.rejects
.toThrow('Payment not found: 999');
});
});
describe('refundPayment', () => {
it('should refund payment successfully', async () => {
// Arrange
const mockPayment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'USD',
status: Payment.Status.COMPLETED,
paymentMethod: 'credit_card',
gatewayTransactionId: 'txn_abc123'
});
mockPaymentRepository.findById.mockResolvedValue(mockPayment);
mockPaymentGateway.refund.mockResolvedValue(true);
mockPaymentRepository.update.mockResolvedValue(true);
mockOrderService.markAsRefunded.mockResolvedValue(true);
// Act
const payment = await paymentService.refundPayment('123');
// Assert
expect(payment.status).toBe(Payment.Status.REFUNDED);
expect(mockPaymentGateway.refund).toHaveBeenCalledWith({
transactionId: 'txn_abc123',
amount: 99.99
});
expect(mockOrderService.markAsRefunded).toHaveBeenCalledWith('456');
});
it('should support partial refunds', async () => {
// Arrange
const mockPayment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 100,
currency: 'USD',
status: Payment.Status.COMPLETED,
paymentMethod: 'credit_card',
gatewayTransactionId: 'txn_abc123'
});
mockPaymentRepository.findById.mockResolvedValue(mockPayment);
mockPaymentGateway.refund.mockResolvedValue(true);
mockPaymentRepository.update.mockResolvedValue(true);
mockOrderService.markAsRefunded.mockResolvedValue(true);
// Act
await paymentService.refundPayment('123', 50);
// Assert
expect(mockPaymentGateway.refund).toHaveBeenCalledWith({
transactionId: 'txn_abc123',
amount: 50
});
});
it('should throw error if payment cannot be refunded', async () => {
// Arrange
const mockPayment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 99.99,
currency: 'USD',
status: Payment.Status.PENDING,
paymentMethod: 'credit_card'
});
mockPaymentRepository.findById.mockResolvedValue(mockPayment);
// Act & Assert
await expect(paymentService.refundPayment('123'))
.rejects
.toThrow('Payment cannot be refunded');
});
it('should throw error if refund amount exceeds payment', async () => {
// Arrange
const mockPayment = new Payment({
id: '123',
orderId: '456',
userId: '789',
amount: 100,
currency: 'USD',
status: Payment.Status.COMPLETED,
paymentMethod: 'credit_card',
gatewayTransactionId: 'txn_abc123'
});
mockPaymentRepository.findById.mockResolvedValue(mockPayment);
// Act & Assert
await expect(paymentService.refundPayment('123', 150))
.rejects
.toThrow('Refund amount exceeds payment amount');
});
});
});
Purpose: Test component interactions
Example:
// tests/integration/payment-flow.test.js
const request = require('supertest');
const app = require('../../src/app');
const db = require('../../src/database');
describe('Payment Flow Integration', () => {
let authToken;
let orderId;
beforeAll(async () => {
await db.connect();
await db.migrate();
});
afterAll(async () => {
await db.close();
});
beforeEach(async () => {
await db.truncate();
// Create test user and get auth token
const userRes = await request(app)
.post('/api/v1/auth/register')
.send({
email: 'test@example.com',
password: 'password123'
});
authToken = userRes.body.data.token;
// Create test order
const orderRes = await request(app)
.post('/api/v1/orders')
.set('Authorization', `Bearer ${authToken}`)
.send({
items: [{ product_id: '123', quantity: 2 }]
});
orderId = orderRes.body.data.id;
});
describe('Successful Payment Flow', () => {
it('should complete payment end-to-end', async () => {
// Create payment
const paymentRes = await request(app)
.post('/api/v1/payments')
.set('Authorization', `Bearer ${authToken}`)
.send({
orderId: orderId,
amount: 99.99,
currency: 'USD',
paymentMethod: 'credit_card',
paymentDetails: {
cardNumber: '4111111111111111',
expMonth: 12,
expYear: 2025,
cvc: '123'
}
});
expect(paymentRes.status).toBe(201);
expect(paymentRes.body.success).toBe(true);
expect(paymentRes.body.data.status).toBe('completed');
const paymentId = paymentRes.body.data.id;
// Verify payment status
const getRes = await request(app)
.get(`/api/v1/payments/${paymentId}`)
.set('Authorization', `Bearer ${authToken}`);
expect(getRes.status).toBe(200);
expect(getRes.body.data.status).toBe('completed');
// Verify order was marked as paid
const orderRes = await request(app)
.get(`/api/v1/orders/${orderId}`)
.set('Authorization', `Bearer ${authToken}`);
expect(orderRes.body.data.status).toBe('paid');
});
});
describe('Failed Payment Flow', () => {
it('should handle payment failure gracefully', async () => {
// Try to create payment with card that will be declined
const paymentRes = await request(app)
.post('/api/v1/payments')
.set('Authorization', `Bearer ${authToken}`)
.send({
orderId: orderId,
amount: 99.99,
currency: 'USD',
paymentMethod: 'credit_card',
paymentDetails: {
cardNumber: '4000000000000002', // Test card that will be declined
expMonth: 12,
expYear: 2025,
cvc: '123'
}
});
expect(paymentRes.status).toBe(400);
expect(paymentRes.body.success).toBe(false);
expect(paymentRes.body.error.code).toBe('PAYMENT_FAILED');
// Verify order status unchanged
const orderRes = await request(app)
.get(`/api/v1/orders/${orderId}`)
.set('Authorization', `Bearer ${authToken}`);
expect(orderRes.body.data.status).toBe('pending');
});
});
});
Purpose: Test HTTP endpoints
Example:
// tests/api/payments.test.js
const request = require('supertest');
const app = require('../../src/app');
describe('Payment API', () => {
let authToken;
beforeAll(async () => {
// Get auth token for tests
const res = await request(app)
.post('/api/v1/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
});
authToken = res.body.data.token;
});
describe('POST /api/v1/payments', () => {
it('should return 401 without authentication', async () => {
const res = await request(app)
.post('/api/v1/payments')
.send({
orderId: '123',
amount: 99.99
});
expect(res.status).toBe(401);
});
it('should return 400 with missing fields', async () => {
const res = await request(app)
.post('/api/v1/payments')
.set('Authorization', `Bearer ${authToken}`)
.send({
orderId: '123'
// Missing amount, currency, paymentMethod
});
expect(res.status).toBe(400);
expect(res.body.error.code).toBe('MISSING_REQUIRED_FIELDS');
});
it('should return 201 with valid data', async () => {
const res = await request(app)
.post('/api/v1/payments')
.set('Authorization', `Bearer ${authToken}`)
.send({
orderId: '123',
amount: 99.99,
currency: 'USD',
paymentMethod: 'credit_card',
paymentDetails: {
cardNumber: '4111111111111111',
expMonth: 12,
expYear: 2025,
cvc: '123'
}
});
expect(res.status).toBe(201);
expect(res.body.success).toBe(true);
expect(res.body.data).toHaveProperty('id');
expect(res.body.data.amount).toBe(99.99);
});
});
describe('GET /api/v1/payments/:id', () => {
it('should return 404 for non-existent payment', async () => {
const res = await request(app)
.get('/api/v1/payments/non-existent')
.set('Authorization', `Bearer ${authToken}`);
expect(res.status).toBe(404);
});
it('should return payment data', async () => {
// First create a payment
const createRes = await request(app)
.post('/api/v1/payments')
.set('Authorization', `Bearer ${authToken}`)
.send({
orderId: '123',
amount: 99.99,
currency: 'USD',
paymentMethod: 'credit_card',
paymentDetails: {}
});
const paymentId = createRes.body.data.id;
// Get the payment
const res = await request(app)
.get(`/api/v1/payments/${paymentId}`)
.set('Authorization', `Bearer ${authToken}`);
expect(res.status).toBe(200);
expect(res.body.data.id).toBe(paymentId);
});
});
});
# Test Suite: [Component Name]
## Summary
- **Component**: [Name]
- **Test File**: [Path]
- **Tests Written**: [N]
- **Coverage**: [X%]
## Test Breakdown
### Unit Tests (N)
- ✅ [Test name 1]
- ✅ [Test name 2]
- ...
### Edge Cases (M)
- ✅ [Edge case 1]
- ✅ [Edge case 2]
- ...
### Error Cases (K)
- ✅ [Error case 1]
- ✅ [Error case 2]
- ...
## Coverage Report
| File | Statements | Branches | Functions | Lines |
|------|------------|----------|-----------|-------|
| Component.js | 95% | 90% | 100% | 94% |
## Next Steps
- Ready for test-runner agent
Before completing:
You are an elite AI agent architect specializing in crafting high-performance agent configurations. Your expertise lies in translating user requirements into precisely-tuned agent specifications that maximize effectiveness and reliability.