Master comprehensive API documentation and testing strategies. Learn OpenAPI/Swagger, automated testing, contract testing, and load testing.
Master comprehensive API documentation and testing strategies. Learn OpenAPI/Swagger, automated testing, contract testing, and load testing.
Create comprehensive API documentation with OpenAPI/Swagger
Implement automated testing strategies with Jest and Supertest
Master contract testing for API reliability
Design load testing scenarios with Artillery
Build API mocking and virtualization tools
Apply documentation-driven development practices
Create comprehensive, interactive API documentation using OpenAPI specifications and Swagger tools.
Learn about OpenAPI Specification Setup.
1# Install Swagger and OpenAPI tools
2npm install swagger-ui-express swagger-jsdoc
3npm install @apidevtools/swagger-parser swagger2openapi
4npm install --save-dev @types/swagger-ui-express1const swaggerJsdoc = require('swagger-jsdoc');
2const swaggerUi = require('swagger-ui-express');
3
4class SwaggerDocumentation {
5 constructor(app, options = {}) {
6 this.app = app;
7 this.options = {
8 title: options.title || 'E-commerce API',
9 version: options.version || '1.0.0',
10 description: options.description || 'Comprehensive e-commerce platform API',
11 baseUrl: options.baseUrl || 'http://localhost:4000',
12 docsPath: options.docsPath || '/api-docs',
13 specPath: options.specPath || '/api-docs.json',
14 ...options
15 };
16
17 this.swaggerDefinition = {
18 openapi: '3.0.0',
19 info: {
20 title: this.options.title,
21 version: this.options.version,
22 description: this.options.description,
23 contact: {
24 name: 'API Support',
25 email: '[email protected]',
26 url: 'https://example.com/support'
27 },
28 license: {
29 name: 'MIT',
30 url: 'https://opensource.org/licenses/MIT'
31 },
32 termsOfService: 'https://example.com/terms'
33 },
34 servers: [
35 {
36 url: this.options.baseUrl,
37 description: 'Development server'
38 },
39 {
40 url: 'https://api.example.com',
41 description: 'Production server'
42 },
43 {
44 url: 'https://staging-api.example.com',
45 description: 'Staging server'
46 }
47 ],
48 components: {
49 securitySchemes: {
50 bearerAuth: {
51 type: 'http',
52 scheme: 'bearer',
53 bearerFormat: 'JWT',
54 description: 'JWT token for authentication'
55 },
56 apiKey: {
57 type: 'apiKey',
58 in: 'header',
59 name: 'X-API-Key',
60 description: 'API key for authentication'
61 },
62 oauth2: {
63 type: 'oauth2',
64 flows: {
65 authorizationCode: {
66 authorizationUrl: 'https://example.com/oauth/authorize',
67 tokenUrl: 'https://example.com/oauth/token',
68 scopes: {
69 'read:users': 'Read user information',
70 'write:users': 'Modify user information',
71 'read:products': 'Read product information',
72 'write:products': 'Modify product information',
73 'read:orders': 'Read order information',
74 'write:orders': 'Modify order information',
75 'admin': 'Administrative access'
76 }
77 }
78 }
79 }
80 },
81 schemas: this.getSchemaDefinitions(),
82 responses: this.getResponseDefinitions(),
83 parameters: this.getParameterDefinitions(),
84 examples: this.getExampleDefinitions()
85 },
86 security: [
87 { bearerAuth: [] },
88 { apiKey: [] }
89 ],
90 tags: [
91 {
92 name: 'Authentication',
93 description: 'User authentication and authorization endpoints'
94 },
95 {
96 name: 'Users',
97 description: 'User management operations'
98 },
99 {
100 name: 'Products',
101 description: 'Product catalog management'
102 },
103 {
104 name: 'Orders',
105 description: 'Order processing and management'
106 },
107 {
108 name: 'Categories',
109 description: 'Product category management'
110 },
111 {
112 name: 'Reviews',
113 description: 'Product review operations'
114 },
115 {
116 name: 'Analytics',
117 description: 'Analytics and reporting endpoints'
118 }
119 ]
120 };
121 }
122
123 getSchemaDefinitions() {
124 return {
125 // User schemas
126 User: {
127 type: 'object',
128 required: ['firstName', 'lastName', 'email'],
129 properties: {
130 id: {
131 type: 'string',
132 format: 'uuid',
133 description: 'Unique identifier for the user',
134 example: '123e4567-e89b-12d3-a456-426614174000'
135 },
136 firstName: {
137 type: 'string',
138 minLength: 2,
139 maxLength: 50,
140 description: 'User first name',
141 example: 'John'
142 },
143 lastName: {
144 type: 'string',
145 minLength: 2,
146 maxLength: 50,
147 description: 'User last name',
148 example: 'Doe'
149 },
150 email: {
151 type: 'string',
152 format: 'email',
153 description: 'User email address',
154 example: '[email protected]'
155 },
156 avatar: {
157 type: 'string',
158 format: 'uri',
159 description: 'URL to user avatar image',
160 example: 'https://example.com/avatars/john.jpg'
161 },
162 role: {
163 type: 'string',
164 enum: ['user', 'admin', 'manager', 'premium'],
165 description: 'User role',
166 example: 'user'
167 },
168 isActive: {
169 type: 'boolean',
170 description: 'Whether the user account is active',
171 example: true
172 },
173 isEmailVerified: {
174 type: 'boolean',
175 description: 'Whether the user email is verified',
176 example: true
177 },
178 createdAt: {
179 type: 'string',
180 format: 'date-time',
181 description: 'User creation timestamp',
182 example: '2024-01-15T10:30:00Z'
183 },
184 updatedAt: {
185 type: 'string',
186 format: 'date-time',
187 description: 'User last update timestamp',
188 example: '2024-01-20T15:45:00Z'
189 }
190 }
191 },
192
193 CreateUserRequest: {
194 type: 'object',
195 required: ['firstName', 'lastName', 'email', 'password'],
196 properties: {
197 firstName: {
198 type: 'string',
199 minLength: 2,
200 maxLength: 50,
201 example: 'John'
202 },
203 lastName: {
204 type: 'string',
205 minLength: 2,
206 maxLength: 50,
207 example: 'Doe'
208 },
209 email: {
210 type: 'string',
211 format: 'email',
212 example: '[email protected]'
213 },
214 password: {
215 type: 'string',
216 minLength: 8,
217 description: 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character',
218 example: 'SecurePass123!'
219 },
220 role: {
221 type: 'string',
222 enum: ['user', 'admin', 'manager', 'premium'],
223 default: 'user',
224 example: 'user'
225 }
226 }
227 },
228
229 // Product schemas
230 Product: {
231 type: 'object',
232 required: ['name', 'price', 'categoryId'],
233 properties: {
234 id: {
235 type: 'string',
236 format: 'uuid',
237 description: 'Unique identifier for the product'
238 },
239 name: {
240 type: 'string',
241 minLength: 1,
242 maxLength: 200,
243 description: 'Product name',
244 example: 'Wireless Bluetooth Headphones'
245 },
246 slug: {
247 type: 'string',
248 pattern: '^[a-z0-9-]+$',
249 description: 'URL-friendly product identifier',
250 example: 'wireless-bluetooth-headphones'
251 },
252 description: {
253 type: 'string',
254 maxLength: 2000,
255 description: 'Detailed product description',
256 example: 'High-quality wireless headphones with noise cancellation'
257 },
258 shortDescription: {
259 type: 'string',
260 maxLength: 500,
261 description: 'Brief product description',
262 example: 'Premium wireless headphones with 30-hour battery life'
263 },
264 sku: {
265 type: 'string',
266 pattern: '^[A-Z0-9-]+$',
267 description: 'Stock keeping unit',
268 example: 'WBH-001'
269 },
270 price: {
271 type: 'number',
272 minimum: 0,
273 multipleOf: 0.01,
274 description: 'Product price in USD',
275 example: 199.99
276 },
277 compareAtPrice: {
278 type: 'number',
279 minimum: 0,
280 multipleOf: 0.01,
281 description: 'Original price for discount calculation',
282 example: 249.99
283 },
284 categoryId: {
285 type: 'string',
286 format: 'uuid',
287 description: 'Product category identifier'
288 },
289 images: {
290 type: 'array',
291 items: {
292 type: 'object',
293 properties: {
294 url: { type: 'string', format: 'uri' },
295 alt: { type: 'string' },
296 position: { type: 'integer' }
297 }
298 },
299 description: 'Product images'
300 },
301 variants: {
302 type: 'array',
303 items: { $ref: '#/components/schemas/ProductVariant' },
304 description: 'Product variants'
305 },
306 isActive: {
307 type: 'boolean',
308 description: 'Whether the product is active',
309 example: true
310 },
311 isFeatured: {
312 type: 'boolean',
313 description: 'Whether the product is featured',
314 example: false
315 },
316 inventory: {
317 $ref: '#/components/schemas/ProductInventory'
318 },
319 createdAt: {
320 type: 'string',
321 format: 'date-time'
322 },
323 updatedAt: {
324 type: 'string',
325 format: 'date-time'
326 }
327 }
328 },
329
330 ProductVariant: {
331 type: 'object',
332 properties: {
333 id: { type: 'string', format: 'uuid' },
334 title: { type: 'string', example: 'Black - Large' },
335 sku: { type: 'string', example: 'WBH-001-BL-L' },
336 price: { type: 'number', example: 199.99 },
337 inventoryQuantity: { type: 'integer', example: 50 },
338 optionValues: {
339 type: 'object',
340 additionalProperties: { type: 'string' },
341 example: { color: 'Black', size: 'Large' }
342 }
343 }
344 },
345
346 ProductInventory: {
347 type: 'object',
348 properties: {
349 quantity: { type: 'integer', example: 100 },
350 trackInventory: { type: 'boolean', example: true },
351 allowBackorder: { type: 'boolean', example: false },
352 stockStatus: {
353 type: 'string',
354 enum: ['in_stock', 'low_stock', 'out_of_stock'],
355 example: 'in_stock'
356 }
357 }
358 },
359
360 // Error schemas
361 ErrorResponse: {
362 type: 'object',
363 required: ['error', 'message'],
364 properties: {
365 error: {
366 type: 'string',
367 description: 'Error type',
368 example: 'Validation Error'
369 },
370 message: {
371 type: 'string',
372 description: 'Human-readable error message',
373 example: 'The provided email address is invalid'
374 },
375 details: {
376 type: 'object',
377 description: 'Additional error details',
378 additionalProperties: true
379 },
380 timestamp: {
381 type: 'string',
382 format: 'date-time',
383 description: 'Error timestamp'
384 },
385 path: {
386 type: 'string',
387 description: 'API endpoint that generated the error',
388 example: '/api/v1/users'
389 }
390 }
391 },
392
393 ValidationError: {
394 allOf: [
395 { $ref: '#/components/schemas/ErrorResponse' },
396 {
397 type: 'object',
398 properties: {
399 details: {
400 type: 'object',
401 properties: {
402 fields: {
403 type: 'array',
404 items: {
405 type: 'object',
406 properties: {
407 field: { type: 'string' },
408 message: { type: 'string' },
409 code: { type: 'string' }
410 }
411 }
412 }
413 }
414 }
415 }
416 }
417 ]
418 },
419
420 // Pagination schemas
421 PaginationMeta: {
422 type: 'object',
423 properties: {
424 currentPage: { type: 'integer', example: 1 },
425 totalPages: { type: 'integer', example: 10 },
426 totalItems: { type: 'integer', example: 200 },
427 itemsPerPage: { type: 'integer', example: 20 },
428 hasNextPage: { type: 'boolean', example: true },
429 hasPrevPage: { type: 'boolean', example: false }
430 }
431 },
432
433 PaginatedResponse: {
434 type: 'object',
435 properties: {
436 data: {
437 type: 'array',
438 items: {}
439 },
440 pagination: {
441 $ref: '#/components/schemas/PaginationMeta'
442 }
443 }
444 }
445 };
446 }
447
448 getResponseDefinitions() {
449 return {
450 NotFound: {
451 description: 'Resource not found',
452 content: {
453 'application/json': {
454 schema: {
455 $ref: '#/components/schemas/ErrorResponse'
456 },
457 example: {
458 error: 'Not Found',
459 message: 'The requested resource was not found',
460 timestamp: '2024-01-15T10:30:00Z',
461 path: '/api/v1/products/invalid-id'
462 }
463 }
464 }
465 },
466 Unauthorized: {
467 description: 'Authentication required',
468 content: {
469 'application/json': {
470 schema: {
471 $ref: '#/components/schemas/ErrorResponse'
472 },
473 example: {
474 error: 'Unauthorized',
475 message: 'Authentication token is required',
476 timestamp: '2024-01-15T10:30:00Z',
477 path: '/api/v1/users/profile'
478 }
479 }
480 }
481 },
482 Forbidden: {
483 description: 'Access denied',
484 content: {
485 'application/json': {
486 schema: {
487 $ref: '#/components/schemas/ErrorResponse'
488 }
489 }
490 }
491 },
492 ValidationError: {
493 description: 'Validation failed',
494 content: {
495 'application/json': {
496 schema: {
497 $ref: '#/components/schemas/ValidationError'
498 }
499 }
500 }
501 },
502 RateLimited: {
503 description: 'Too many requests',
504 headers: {
505 'X-Rate-Limit-Remaining': {
506 schema: { type: 'integer' },
507 description: 'Number of requests remaining'
508 },
509 'X-Rate-Limit-Reset': {
510 schema: { type: 'integer' },
511 description: 'Time when rate limit resets (Unix timestamp)'
512 }
513 },
514 content: {
515 'application/json': {
516 schema: {
517 $ref: '#/components/schemas/ErrorResponse'
518 }
519 }
520 }
521 }
522 };
523 }
524
525 getParameterDefinitions() {
526 return {
527 PageParam: {
528 name: 'page',
529 in: 'query',
530 description: 'Page number for pagination',
531 schema: {
532 type: 'integer',
533 minimum: 1,
534 default: 1
535 },
536 example: 1
537 },
538 LimitParam: {
539 name: 'limit',
540 in: 'query',
541 description: 'Number of items per page',
542 schema: {
543 type: 'integer',
544 minimum: 1,
545 maximum: 100,
546 default: 20
547 },
548 example: 20
549 },
550 SortParam: {
551 name: 'sort',
552 in: 'query',
553 description: 'Sort field and order',
554 schema: {
555 type: 'string',
556 pattern: '^[a-zA-Z0-9_]+:(asc|desc)$'
557 },
558 example: 'createdAt:desc'
559 },
560 SearchParam: {
561 name: 'search',
562 in: 'query',
563 description: 'Search query string',
564 schema: {
565 type: 'string',
566 minLength: 1,
567 maxLength: 100
568 },
569 example: 'wireless headphones'
570 }
571 };
572 }
573
574 getExampleDefinitions() {
575 return {
576 UserCreateExample: {
577 summary: 'Create a new user',
578 value: {
579 firstName: 'John',
580 lastName: 'Doe',
581 email: '[email protected]',
582 password: 'SecurePass123!',
583 role: 'user'
584 }
585 },
586 ProductCreateExample: {
587 summary: 'Create a new product',
588 value: {
589 name: 'Wireless Bluetooth Headphones',
590 description: 'High-quality wireless headphones with noise cancellation and 30-hour battery life',
591 shortDescription: 'Premium wireless headphones with long battery life',
592 price: 199.99,
593 compareAtPrice: 249.99,
594 categoryId: '123e4567-e89b-12d3-a456-426614174000',
595 sku: 'WBH-001'
596 }
597 }
598 };
599 }
600
601 setupSwagger() {
602 const options = {
603 definition: this.swaggerDefinition,
604 apis: [
605 './routes/api/v1/*.js',
606 './controllers/*.js',
607 './models/*.js'
608 ]
609 };
610
611 const specs = swaggerJsdoc(options);
612
613 // Serve Swagger UI
614 this.app.use(
615 this.options.docsPath,
616 swaggerUi.serve,
617 swaggerUi.setup(specs, {
618 explorer: true,
619 customCss: '.swagger-ui .topbar { display: none }',
620 customSiteTitle: this.options.title + ' - API Documentation',
621 swaggerOptions: {
622 persistAuthorization: true,
623 displayRequestDuration: true,
624 docExpansion: 'none',
625 filter: true,
626 showExtensions: true,
627 showCommonExtensions: true,
628 tryItOutEnabled: true
629 }
630 })
631 );
632
633 // Serve OpenAPI spec as JSON
634 this.app.get(this.options.specPath, (req, res) => {
635 res.setHeader('Content-Type', 'application/json');
636 res.send(specs);
637 });
638
639 console.log(\`📖 Swagger documentation available at \${this.options.docsPath}\`);
640 console.log(\`📄 OpenAPI specification available at \${this.options.specPath}\`);
641
642 return specs;
643 }
644
645 // Generate documentation from code annotations
646 generateAnnotations() {
647 return {
648 userRoutes: \`
649/**
650 * @swagger
651 * /api/v1/users:
652 * get:
653 * summary: Get all users
654 * description: Retrieve a paginated list of users with optional filtering
655 * tags: [Users]
656 * security:
657 * - bearerAuth: []
658 * parameters:
659 * - $ref: '#/components/parameters/PageParam'
660 * - $ref: '#/components/parameters/LimitParam'
661 * - $ref: '#/components/parameters/SortParam'
662 * - $ref: '#/components/parameters/SearchParam'
663 * - name: role
664 * in: query
665 * description: Filter by user role
666 * schema:
667 * type: string
668 * enum: [user, admin, manager, premium]
669 * - name: isActive
670 * in: query
671 * description: Filter by active status
672 * schema:
673 * type: boolean
674 * responses:
675 * 200:
676 * description: Successfully retrieved users
677 * content:
678 * application/json:
679 * schema:
680 * allOf:
681 * - $ref: '#/components/schemas/PaginatedResponse'
682 * - type: object
683 * properties:
684 * data:
685 * type: array
686 * items:
687 * $ref: '#/components/schemas/User'
688 * 401:
689 * $ref: '#/components/responses/Unauthorized'
690 * 403:
691 * $ref: '#/components/responses/Forbidden'
692 * 429:
693 * $ref: '#/components/responses/RateLimited'
694 *
695 * post:
696 * summary: Create a new user
697 * description: Create a new user account
698 * tags: [Users]
699 * security:
700 * - bearerAuth: []
701 * requestBody:
702 * required: true
703 * content:
704 * application/json:
705 * schema:
706 * $ref: '#/components/schemas/CreateUserRequest'
707 * examples:
708 * default:
709 * $ref: '#/components/examples/UserCreateExample'
710 * responses:
711 * 201:
712 * description: User created successfully
713 * content:
714 * application/json:
715 * schema:
716 * type: object
717 * properties:
718 * user:
719 * $ref: '#/components/schemas/User'
720 * message:
721 * type: string
722 * example: User created successfully
723 * 400:
724 * $ref: '#/components/responses/ValidationError'
725 * 401:
726 * $ref: '#/components/responses/Unauthorized'
727 * 409:
728 * description: User already exists
729 * content:
730 * application/json:
731 * schema:
732 * $ref: '#/components/schemas/ErrorResponse'
733 */
734 \`,
735
736 productRoutes: \`
737/**
738 * @swagger
739 * /api/v1/products/{id}:
740 * get:
741 * summary: Get product by ID
742 * description: Retrieve a specific product by its unique identifier
743 * tags: [Products]
744 * parameters:
745 * - name: id
746 * in: path
747 * required: true
748 * description: Product unique identifier
749 * schema:
750 * type: string
751 * format: uuid
752 * - name: include
753 * in: query
754 * description: Include related data
755 * schema:
756 * type: array
757 * items:
758 * type: string
759 * enum: [variants, category, reviews, images]
760 * style: form
761 * explode: false
762 * responses:
763 * 200:
764 * description: Product found
765 * content:
766 * application/json:
767 * schema:
768 * $ref: '#/components/schemas/Product'
769 * 404:
770 * $ref: '#/components/responses/NotFound'
771 * 429:
772 * $ref: '#/components/responses/RateLimited'
773 *
774 * put:
775 * summary: Update product
776 * description: Update an existing product (full replacement)
777 * tags: [Products]
778 * security:
779 * - bearerAuth: []
780 * parameters:
781 * - name: id
782 * in: path
783 * required: true
784 * schema:
785 * type: string
786 * format: uuid
787 * requestBody:
788 * required: true
789 * content:
790 * application/json:
791 * schema:
792 * $ref: '#/components/schemas/Product'
793 * responses:
794 * 200:
795 * description: Product updated successfully
796 * content:
797 * application/json:
798 * schema:
799 * $ref: '#/components/schemas/Product'
800 * 400:
801 * $ref: '#/components/responses/ValidationError'
802 * 401:
803 * $ref: '#/components/responses/Unauthorized'
804 * 403:
805 * $ref: '#/components/responses/Forbidden'
806 * 404:
807 * $ref: '#/components/responses/NotFound'
808 */
809 \`
810 };
811 }
812}
813
814module.exports = SwaggerDocumentation;Implement comprehensive automated testing strategies for API endpoints and business logic.
1# Install testing dependencies
2npm install --save-dev jest supertest
3npm install --save-dev @types/jest @types/supertest
4npm install --save-dev jest-environment-node
5npm install --save-dev mongodb-memory-server1const { MongoMemoryServer } = require('mongodb-memory-server');
2const mongoose = require('mongoose');
3const Redis = require('ioredis-mock');
4
5class TestEnvironment {
6 constructor() {
7 this.mongoServer = null;
8 this.redisClient = null;
9 this.testDatabase = null;
10 }
11
12 async setup() {
13 console.log('🔧 Setting up test environment...');
14
15 // Setup in-memory MongoDB
16 await this.setupTestDatabase();
17
18 // Setup mock Redis
19 await this.setupMockRedis();
20
21 // Setup test data
22 await this.seedTestData();
23
24 console.log('✅ Test environment ready');
25 }
26
27 async setupTestDatabase() {
28 try {
29 // Create in-memory MongoDB instance
30 this.mongoServer = await MongoMemoryServer.create({
31 instance: {
32 dbName: 'test_database',
33 port: 27018
34 }
35 });
36
37 const mongoUri = this.mongoServer.getUri();
38
39 // Connect with mongoose
40 await mongoose.connect(mongoUri, {
41 useNewUrlParser: true,
42 useUnifiedTopology: true
43 });
44
45 this.testDatabase = mongoose.connection.db;
46
47 console.log('📝 In-memory MongoDB connected');
48
49 } catch (error) {
50 console.error('❌ Failed to setup test database:', error);
51 throw error;
52 }
53 }
54
55 async setupMockRedis() {
56 try {
57 // Use ioredis-mock for Redis testing
58 this.redisClient = new Redis();
59
60 // Mock Redis methods for testing
61 global.mockRedis = this.redisClient;
62
63 console.log('📝 Mock Redis client created');
64
65 } catch (error) {
66 console.error('❌ Failed to setup mock Redis:', error);
67 throw error;
68 }
69 }
70
71 async seedTestData() {
72 try {
73 const testUsers = [
74 {
75 _id: new mongoose.Types.ObjectId('507f1f77bcf86cd799439011'),
76 firstName: 'John',
77 lastName: 'Doe',
78 email: '[email protected]',
79 password: 'hashedpassword123',
80 role: 'user',
81 isActive: true,
82 isEmailVerified: true,
83 createdAt: new Date('2024-01-01'),
84 updatedAt: new Date('2024-01-01')
85 },
86 {
87 _id: new mongoose.Types.ObjectId('507f1f77bcf86cd799439012'),
88 firstName: 'Jane',
89 lastName: 'Smith',
90 email: '[email protected]',
91 password: 'hashedpassword456',
92 role: 'admin',
93 isActive: true,
94 isEmailVerified: true,
95 createdAt: new Date('2024-01-01'),
96 updatedAt: new Date('2024-01-01')
97 }
98 ];
99
100 const testCategories = [
101 {
102 _id: new mongoose.Types.ObjectId('507f1f77bcf86cd799439021'),
103 name: 'Electronics',
104 slug: 'electronics',
105 description: 'Electronic devices and accessories',
106 isActive: true,
107 sortOrder: 1,
108 createdAt: new Date('2024-01-01'),
109 updatedAt: new Date('2024-01-01')
110 }
111 ];
112
113 const testProducts = [
114 {
115 _id: new mongoose.Types.ObjectId('507f1f77bcf86cd799439031'),
116 name: 'Test Product 1',
117 slug: 'test-product-1',
118 description: 'This is a test product',
119 sku: 'TEST-001',
120 price: 99.99,
121 categoryId: testCategories[0]._id,
122 isActive: true,
123 isFeatured: false,
124 inventoryQuantity: 100,
125 trackInventory: true,
126 createdAt: new Date('2024-01-01'),
127 updatedAt: new Date('2024-01-01')
128 },
129 {
130 _id: new mongoose.Types.ObjectId('507f1f77bcf86cd799439032'),
131 name: 'Test Product 2',
132 slug: 'test-product-2',
133 description: 'This is another test product',
134 sku: 'TEST-002',
135 price: 149.99,
136 categoryId: testCategories[0]._id,
137 isActive: true,
138 isFeatured: true,
139 inventoryQuantity: 50,
140 trackInventory: true,
141 createdAt: new Date('2024-01-01'),
142 updatedAt: new Date('2024-01-01')
143 }
144 ];
145
146 // Insert test data
147 await this.testDatabase.collection('users').insertMany(testUsers);
148 await this.testDatabase.collection('categories').insertMany(testCategories);
149 await this.testDatabase.collection('products').insertMany(testProducts);
150
151 console.log('📝 Test data seeded');
152
153 } catch (error) {
154 console.error('❌ Failed to seed test data:', error);
155 throw error;
156 }
157 }
158
159 async teardown() {
160 console.log('🧹 Tearing down test environment...');
161
162 try {
163 // Close database connections
164 if (mongoose.connection.readyState === 1) {
165 await mongoose.connection.dropDatabase();
166 await mongoose.connection.close();
167 }
168
169 // Stop MongoDB server
170 if (this.mongoServer) {
171 await this.mongoServer.stop();
172 }
173
174 // Close Redis connection
175 if (this.redisClient) {
176 await this.redisClient.quit();
177 }
178
179 console.log('✅ Test environment cleaned up');
180
181 } catch (error) {
182 console.error('❌ Error during teardown:', error);
183 }
184 }
185
186 async clearTestData() {
187 try {
188 // Clear all collections
189 const collections = await this.testDatabase.listCollections().toArray();
190
191 for (const collection of collections) {
192 await this.testDatabase.collection(collection.name).deleteMany({});
193 }
194
195 // Re-seed fresh test data
196 await this.seedTestData();
197
198 } catch (error) {
199 console.error('❌ Error clearing test data:', error);
200 throw error;
201 }
202 }
203
204 // Test utilities
205 createTestUser(overrides = {}) {
206 return {
207 firstName: 'Test',
208 lastName: 'User',
209 email: '[email protected]',
210 password: 'TestPass123!',
211 role: 'user',
212 ...overrides
213 };
214 }
215
216 createTestProduct(overrides = {}) {
217 return {
218 name: 'Test Product',
219 description: 'A test product description',
220 sku: 'TEST-SKU',
221 price: 99.99,
222 categoryId: '507f1f77bcf86cd799439021',
223 isActive: true,
224 ...overrides
225 };
226 }
227
228 generateAuthToken(userId = '507f1f77bcf86cd799439011') {
229 const jwt = require('jsonwebtoken');
230 return jwt.sign(
231 { userId, role: 'user' },
232 process.env.JWT_SECRET || 'test-secret',
233 { expiresIn: '1h' }
234 );
235 }
236
237 generateAdminToken(userId = '507f1f77bcf86cd799439012') {
238 const jwt = require('jsonwebtoken');
239 return jwt.sign(
240 { userId, role: 'admin' },
241 process.env.JWT_SECRET || 'test-secret',
242 { expiresIn: '1h' }
243 );
244 }
245}
246
247module.exports = new TestEnvironment();Create comprehensive integration tests for API endpoints with proper setup and teardown.
1const request = require('supertest');
2const app = require('../../server/app');
3const testEnv = require('../setup/test-setup');
4
5describe('Users API', () => {
6 let authToken;
7 let adminToken;
8
9 beforeAll(async () => {
10 await testEnv.setup();
11 authToken = testEnv.generateAuthToken();
12 adminToken = testEnv.generateAdminToken();
13 });
14
15 afterAll(async () => {
16 await testEnv.teardown();
17 });
18
19 beforeEach(async () => {
20 await testEnv.clearTestData();
21 });
22
23 describe('GET /api/v1/users', () => {
24 it('should require authentication', async () => {
25 const response = await request(app)
26 .get('/api/v1/users')
27 .expect(401);
28
29 expect(response.body).toHaveProperty('error', 'Unauthorized');
30 expect(response.body).toHaveProperty('message');
31 });
32
33 it('should require admin role', async () => {
34 const response = await request(app)
35 .get('/api/v1/users')
36 .set('Authorization', \`Bearer \${authToken}\`)
37 .expect(403);
38
39 expect(response.body).toHaveProperty('error', 'Forbidden');
40 });
41
42 it('should return paginated users for admin', async () => {
43 const response = await request(app)
44 .get('/api/v1/users')
45 .set('Authorization', \`Bearer \${adminToken}\`)
46 .expect(200);
47
48 expect(response.body).toHaveProperty('data');
49 expect(response.body).toHaveProperty('pagination');
50 expect(Array.isArray(response.body.data)).toBe(true);
51
52 // Check pagination structure
53 expect(response.body.pagination).toHaveProperty('currentPage');
54 expect(response.body.pagination).toHaveProperty('totalPages');
55 expect(response.body.pagination).toHaveProperty('totalItems');
56 expect(response.body.pagination).toHaveProperty('itemsPerPage');
57
58 // Verify user data structure
59 if (response.body.data.length > 0) {
60 const user = response.body.data[0];
61 expect(user).toHaveProperty('id');
62 expect(user).toHaveProperty('firstName');
63 expect(user).toHaveProperty('lastName');
64 expect(user).toHaveProperty('email');
65 expect(user).toHaveProperty('role');
66 expect(user).not.toHaveProperty('password'); // Should not include password
67 }
68 });
69
70 it('should support pagination parameters', async () => {
71 const response = await request(app)
72 .get('/api/v1/users')
73 .query({ page: 1, limit: 1 })
74 .set('Authorization', \`Bearer \${adminToken}\`)
75 .expect(200);
76
77 expect(response.body.pagination.currentPage).toBe(1);
78 expect(response.body.pagination.itemsPerPage).toBe(1);
79 expect(response.body.data.length).toBeLessThanOrEqual(1);
80 });
81
82 it('should support search functionality', async () => {
83 const response = await request(app)
84 .get('/api/v1/users')
85 .query({ search: 'john' })
86 .set('Authorization', \`Bearer \${adminToken}\`)
87 .expect(200);
88
89 // Should find users matching the search term
90 response.body.data.forEach(user => {
91 const fullName = \`\${user.firstName} \${user.lastName} \${user.email}\`.toLowerCase();
92 expect(fullName).toContain('john');
93 });
94 });
95
96 it('should support role filtering', async () => {
97 const response = await request(app)
98 .get('/api/v1/users')
99 .query({ role: 'admin' })
100 .set('Authorization', \`Bearer \${adminToken}\`)
101 .expect(200);
102
103 response.body.data.forEach(user => {
104 expect(user.role).toBe('admin');
105 });
106 });
107
108 it('should include proper cache headers', async () => {
109 const response = await request(app)
110 .get('/api/v1/users')
111 .set('Authorization', \`Bearer \${adminToken}\`)
112 .expect(200);
113
114 expect(response.headers).toHaveProperty('cache-control');
115 expect(response.headers).toHaveProperty('x-total-count');
116 });
117 });
118
119 describe('POST /api/v1/users', () => {
120 const validUserData = {
121 firstName: 'New',
122 lastName: 'User',
123 email: '[email protected]',
124 password: 'SecurePass123!',
125 role: 'user'
126 };
127
128 it('should require authentication', async () => {
129 const response = await request(app)
130 .post('/api/v1/users')
131 .send(validUserData)
132 .expect(401);
133
134 expect(response.body).toHaveProperty('error', 'Unauthorized');
135 });
136
137 it('should require admin role', async () => {
138 const response = await request(app)
139 .post('/api/v1/users')
140 .set('Authorization', \`Bearer \${authToken}\`)
141 .send(validUserData)
142 .expect(403);
143
144 expect(response.body).toHaveProperty('error', 'Forbidden');
145 });
146
147 it('should create a new user with valid data', async () => {
148 const response = await request(app)
149 .post('/api/v1/users')
150 .set('Authorization', \`Bearer \${adminToken}\`)
151 .send(validUserData)
152 .expect(201);
153
154 expect(response.body).toHaveProperty('user');
155 expect(response.body).toHaveProperty('message', 'User created successfully');
156
157 const user = response.body.user;
158 expect(user).toHaveProperty('id');
159 expect(user.firstName).toBe(validUserData.firstName);
160 expect(user.lastName).toBe(validUserData.lastName);
161 expect(user.email).toBe(validUserData.email);
162 expect(user.role).toBe(validUserData.role);
163 expect(user).not.toHaveProperty('password');
164
165 // Check Location header
166 expect(response.headers).toHaveProperty('location');
167 expect(response.headers.location).toMatch(/\\/api\\/v1\\/users\\/.+/);
168 });
169
170 it('should validate required fields', async () => {
171 const invalidData = { ...validUserData };
172 delete invalidData.email;
173
174 const response = await request(app)
175 .post('/api/v1/users')
176 .set('Authorization', \`Bearer \${adminToken}\`)
177 .send(invalidData)
178 .expect(422);
179
180 expect(response.body).toHaveProperty('error', 'Validation Error');
181 expect(response.body).toHaveProperty('details');
182 });
183
184 it('should validate email format', async () => {
185 const invalidData = { ...validUserData, email: 'invalid-email' };
186
187 const response = await request(app)
188 .post('/api/v1/users')
189 .set('Authorization', \`Bearer \${adminToken}\`)
190 .send(invalidData)
191 .expect(422);
192
193 expect(response.body.details).toContain('email');
194 });
195
196 it('should validate password strength', async () => {
197 const invalidData = { ...validUserData, password: 'weak' };
198
199 const response = await request(app)
200 .post('/api/v1/users')
201 .set('Authorization', \`Bearer \${adminToken}\`)
202 .send(invalidData)
203 .expect(422);
204
205 expect(response.body.details).toContain('password');
206 });
207
208 it('should prevent duplicate email addresses', async () => {
209 // Create first user
210 await request(app)
211 .post('/api/v1/users')
212 .set('Authorization', \`Bearer \${adminToken}\`)
213 .send(validUserData)
214 .expect(201);
215
216 // Try to create second user with same email
217 const response = await request(app)
218 .post('/api/v1/users')
219 .set('Authorization', \`Bearer \${adminToken}\`)
220 .send(validUserData)
221 .expect(409);
222
223 expect(response.body).toHaveProperty('error', 'Conflict');
224 expect(response.body.message).toContain('email');
225 });
226
227 it('should set default role when not provided', async () => {
228 const userData = { ...validUserData };
229 delete userData.role;
230
231 const response = await request(app)
232 .post('/api/v1/users')
233 .set('Authorization', \`Bearer \${adminToken}\`)
234 .send(userData)
235 .expect(201);
236
237 expect(response.body.user.role).toBe('user');
238 });
239 });
240
241 describe('GET /api/v1/users/:id', () => {
242 it('should return user by valid ID', async () => {
243 const response = await request(app)
244 .get('/api/v1/users/507f1f77bcf86cd799439011')
245 .set('Authorization', \`Bearer \${adminToken}\`)
246 .expect(200);
247
248 expect(response.body).toHaveProperty('id', '507f1f77bcf86cd799439011');
249 expect(response.body).toHaveProperty('firstName');
250 expect(response.body).toHaveProperty('lastName');
251 expect(response.body).toHaveProperty('email');
252 });
253
254 it('should return 404 for non-existent user', async () => {
255 const response = await request(app)
256 .get('/api/v1/users/507f1f77bcf86cd799439999')
257 .set('Authorization', \`Bearer \${adminToken}\`)
258 .expect(404);
259
260 expect(response.body).toHaveProperty('error', 'Not Found');
261 });
262
263 it('should return 400 for invalid ID format', async () => {
264 const response = await request(app)
265 .get('/api/v1/users/invalid-id')
266 .set('Authorization', \`Bearer \${adminToken}\`)
267 .expect(400);
268
269 expect(response.body).toHaveProperty('error', 'Bad Request');
270 });
271
272 it('should allow users to view their own profile', async () => {
273 const response = await request(app)
274 .get('/api/v1/users/507f1f77bcf86cd799439011')
275 .set('Authorization', \`Bearer \${authToken}\`)
276 .expect(200);
277
278 expect(response.body).toHaveProperty('id', '507f1f77bcf86cd799439011');
279 });
280
281 it('should prevent users from viewing other profiles', async () => {
282 const response = await request(app)
283 .get('/api/v1/users/507f1f77bcf86cd799439012')
284 .set('Authorization', \`Bearer \${authToken}\`)
285 .expect(403);
286
287 expect(response.body).toHaveProperty('error', 'Forbidden');
288 });
289 });
290
291 describe('PUT /api/v1/users/:id', () => {
292 const updateData = {
293 firstName: 'Updated',
294 lastName: 'Name',
295 avatar: 'https://example.com/avatar.jpg'
296 };
297
298 it('should update user with valid data', async () => {
299 const response = await request(app)
300 .put('/api/v1/users/507f1f77bcf86cd799439011')
301 .set('Authorization', \`Bearer \${adminToken}\`)
302 .send(updateData)
303 .expect(200);
304
305 expect(response.body).toHaveProperty('firstName', updateData.firstName);
306 expect(response.body).toHaveProperty('lastName', updateData.lastName);
307 expect(response.body).toHaveProperty('avatar', updateData.avatar);
308 });
309
310 it('should allow users to update their own profile', async () => {
311 const response = await request(app)
312 .put('/api/v1/users/507f1f77bcf86cd799439011')
313 .set('Authorization', \`Bearer \${authToken}\`)
314 .send(updateData)
315 .expect(200);
316
317 expect(response.body).toHaveProperty('firstName', updateData.firstName);
318 });
319
320 it('should prevent users from updating other profiles', async () => {
321 const response = await request(app)
322 .put('/api/v1/users/507f1f77bcf86cd799439012')
323 .set('Authorization', \`Bearer \${authToken}\`)
324 .send(updateData)
325 .expect(403);
326
327 expect(response.body).toHaveProperty('error', 'Forbidden');
328 });
329
330 it('should validate update data', async () => {
331 const invalidData = { firstName: 'X' }; // Too short
332
333 const response = await request(app)
334 .put('/api/v1/users/507f1f77bcf86cd799439011')
335 .set('Authorization', \`Bearer \${adminToken}\`)
336 .send(invalidData)
337 .expect(422);
338
339 expect(response.body).toHaveProperty('error', 'Validation Error');
340 });
341
342 it('should not allow updating sensitive fields', async () => {
343 const sensitiveData = {
344 password: 'newpassword',
345 role: 'admin',
346 isActive: false
347 };
348
349 const response = await request(app)
350 .put('/api/v1/users/507f1f77bcf86cd799439011')
351 .set('Authorization', \`Bearer \${authToken}\`)
352 .send(sensitiveData)
353 .expect(200);
354
355 // These fields should not be updated
356 expect(response.body.role).not.toBe('admin');
357 expect(response.body.isActive).not.toBe(false);
358 });
359 });
360
361 describe('Rate Limiting', () => {
362 it('should enforce rate limits', async () => {
363 const promises = [];
364
365 // Make many requests quickly
366 for (let i = 0; i < 102; i++) {
367 promises.push(
368 request(app)
369 .get('/api/v1/users')
370 .set('Authorization', \`Bearer \${adminToken}\`)
371 );
372 }
373
374 const responses = await Promise.all(promises);
375
376 // Some responses should be rate limited
377 const rateLimitedResponses = responses.filter(res => res.status === 429);
378 expect(rateLimitedResponses.length).toBeGreaterThan(0);
379
380 // Check rate limit headers
381 const limitedResponse = rateLimitedResponses[0];
382 expect(limitedResponse.headers).toHaveProperty('x-rate-limit-remaining');
383 expect(limitedResponse.headers).toHaveProperty('x-rate-limit-reset');
384 });
385 });
386
387 describe('Error Handling', () => {
388 it('should handle database connection errors gracefully', async () => {
389 // Simulate database error by closing connection
390 const mongoose = require('mongoose');
391 await mongoose.connection.close();
392
393 const response = await request(app)
394 .get('/api/v1/users')
395 .set('Authorization', \`Bearer \${adminToken}\`)
396 .expect(500);
397
398 expect(response.body).toHaveProperty('error', 'Internal Server Error');
399
400 // Reconnect for other tests
401 await testEnv.setup();
402 });
403
404 it('should include request ID in error responses', async () => {
405 const response = await request(app)
406 .get('/api/v1/users/invalid-id')
407 .set('Authorization', \`Bearer \${adminToken}\`)
408 .expect(400);
409
410 expect(response.body).toHaveProperty('requestId');
411 expect(response.body).toHaveProperty('timestamp');
412 });
413 });
414});Implement consumer-driven contract testing to ensure API compatibility between services.
1# Install Pact for contract testing
2npm install --save-dev @pact-foundation/pact
3npm install --save-dev @pact-foundation/pact-node1const { Pact } = require('@pact-foundation/pact');
2const { like, eachLike, term } = require('@pact-foundation/pact').Matchers;
3const path = require('path');
4
5describe('User API Contract', () => {
6 let provider;
7
8 beforeAll(() => {
9 provider = new Pact({
10 consumer: 'Frontend App',
11 provider: 'User API',
12 port: 1234,
13 log: path.resolve(process.cwd(), 'logs', 'pact.log'),
14 dir: path.resolve(process.cwd(), 'pacts'),
15 logLevel: 'INFO',
16 spec: 3
17 });
18
19 return provider.setup();
20 });
21
22 afterAll(() => {
23 return provider.finalize();
24 });
25
26 afterEach(() => {
27 return provider.verify();
28 });
29
30 describe('GET /api/v1/users', () => {
31 beforeEach(() => {
32 const userListResponse = {
33 data: eachLike({
34 id: like('507f1f77bcf86cd799439011'),
35 firstName: like('John'),
36 lastName: like('Doe'),
37 email: like('[email protected]'),
38 role: term({ matcher: 'user|admin|manager|premium', generate: 'user' }),
39 isActive: like(true),
40 isEmailVerified: like(true),
41 createdAt: like('2024-01-15T10:30:00Z'),
42 updatedAt: like('2024-01-15T10:30:00Z')
43 }),
44 pagination: like({
45 currentPage: 1,
46 totalPages: 5,
47 totalItems: 100,
48 itemsPerPage: 20,
49 hasNextPage: true,
50 hasPrevPage: false
51 })
52 };
53
54 return provider
55 .given('users exist')
56 .uponReceiving('a request for all users')
57 .withRequest({
58 method: 'GET',
59 path: '/api/v1/users',
60 headers: {
61 Authorization: like('Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'),
62 Accept: 'application/json'
63 },
64 query: {
65 page: '1',
66 limit: '20'
67 }
68 })
69 .willRespondWith({
70 status: 200,
71 headers: {
72 'Content-Type': 'application/json',
73 'X-Total-Count': like('100')
74 },
75 body: userListResponse
76 });
77 });
78
79 it('should return a list of users', async () => {
80 const axios = require('axios');
81
82 const response = await axios.get('http://localhost:1234/api/v1/users', {
83 headers: {
84 Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
85 Accept: 'application/json'
86 },
87 params: {
88 page: 1,
89 limit: 20
90 }
91 });
92
93 expect(response.status).toBe(200);
94 expect(response.data).toHaveProperty('data');
95 expect(response.data).toHaveProperty('pagination');
96 expect(Array.isArray(response.data.data)).toBe(true);
97
98 if (response.data.data.length > 0) {
99 const user = response.data.data[0];
100 expect(user).toHaveProperty('id');
101 expect(user).toHaveProperty('firstName');
102 expect(user).toHaveProperty('lastName');
103 expect(user).toHaveProperty('email');
104 expect(user).toHaveProperty('role');
105 }
106 });
107 });
108
109 describe('POST /api/v1/users', () => {
110 beforeEach(() => {
111 const createUserResponse = {
112 user: like({
113 id: like('507f1f77bcf86cd799439013'),
114 firstName: like('Jane'),
115 lastName: like('Smith'),
116 email: like('[email protected]'),
117 role: like('user'),
118 isActive: like(true),
119 isEmailVerified: like(false),
120 createdAt: like('2024-01-15T10:30:00Z'),
121 updatedAt: like('2024-01-15T10:30:00Z')
122 }),
123 message: like('User created successfully')
124 };
125
126 return provider
127 .given('admin is authenticated')
128 .uponReceiving('a request to create a user')
129 .withRequest({
130 method: 'POST',
131 path: '/api/v1/users',
132 headers: {
133 Authorization: like('Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'),
134 'Content-Type': 'application/json'
135 },
136 body: {
137 firstName: 'Jane',
138 lastName: 'Smith',
139 email: '[email protected]',
140 password: 'SecurePass123!',
141 role: 'user'
142 }
143 })
144 .willRespondWith({
145 status: 201,
146 headers: {
147 'Content-Type': 'application/json',
148 Location: like('/api/v1/users/507f1f77bcf86cd799439013')
149 },
150 body: createUserResponse
151 });
152 });
153
154 it('should create a new user', async () => {
155 const axios = require('axios');
156
157 const userData = {
158 firstName: 'Jane',
159 lastName: 'Smith',
160 email: '[email protected]',
161 password: 'SecurePass123!',
162 role: 'user'
163 };
164
165 const response = await axios.post('http://localhost:1234/api/v1/users', userData, {
166 headers: {
167 Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
168 'Content-Type': 'application/json'
169 }
170 });
171
172 expect(response.status).toBe(201);
173 expect(response.data).toHaveProperty('user');
174 expect(response.data).toHaveProperty('message', 'User created successfully');
175 expect(response.headers).toHaveProperty('location');
176
177 const user = response.data.user;
178 expect(user).toHaveProperty('id');
179 expect(user.firstName).toBe(userData.firstName);
180 expect(user.lastName).toBe(userData.lastName);
181 expect(user.email).toBe(userData.email);
182 expect(user.role).toBe(userData.role);
183 expect(user).not.toHaveProperty('password');
184 });
185 });
186
187 describe('Error Scenarios', () => {
188 beforeEach(() => {
189 return provider
190 .given('user does not exist')
191 .uponReceiving('a request for a non-existent user')
192 .withRequest({
193 method: 'GET',
194 path: '/api/v1/users/507f1f77bcf86cd799439999',
195 headers: {
196 Authorization: like('Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'),
197 Accept: 'application/json'
198 }
199 })
200 .willRespondWith({
201 status: 404,
202 headers: {
203 'Content-Type': 'application/json'
204 },
205 body: {
206 error: like('Not Found'),
207 message: like('User with ID 507f1f77bcf86cd799439999 was not found'),
208 timestamp: like('2024-01-15T10:30:00Z'),
209 path: like('/api/v1/users/507f1f77bcf86cd799439999')
210 }
211 });
212 });
213
214 it('should return 404 for non-existent user', async () => {
215 const axios = require('axios');
216
217 try {
218 await axios.get('http://localhost:1234/api/v1/users/507f1f77bcf86cd799439999', {
219 headers: {
220 Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
221 Accept: 'application/json'
222 }
223 });
224 } catch (error) {
225 expect(error.response.status).toBe(404);
226 expect(error.response.data).toHaveProperty('error', 'Not Found');
227 expect(error.response.data).toHaveProperty('message');
228 expect(error.response.data).toHaveProperty('timestamp');
229 expect(error.response.data).toHaveProperty('path');
230 }
231 });
232 });
233
234 describe('Authentication', () => {
235 beforeEach(() => {
236 return provider
237 .given('user is not authenticated')
238 .uponReceiving('a request without authentication')
239 .withRequest({
240 method: 'GET',
241 path: '/api/v1/users',
242 headers: {
243 Accept: 'application/json'
244 }
245 })
246 .willRespondWith({
247 status: 401,
248 headers: {
249 'Content-Type': 'application/json'
250 },
251 body: {
252 error: like('Unauthorized'),
253 message: like('Authentication token is required'),
254 timestamp: like('2024-01-15T10:30:00Z'),
255 path: like('/api/v1/users')
256 }
257 });
258 });
259
260 it('should return 401 when not authenticated', async () => {
261 const axios = require('axios');
262
263 try {
264 await axios.get('http://localhost:1234/api/v1/users', {
265 headers: {
266 Accept: 'application/json'
267 }
268 });
269 } catch (error) {
270 expect(error.response.status).toBe(401);
271 expect(error.response.data).toHaveProperty('error', 'Unauthorized');
272 expect(error.response.data).toHaveProperty('message');
273 }
274 });
275 });
276});Implement comprehensive load testing to ensure API performance under various traffic conditions.
1# Install Artillery for load testing
2npm install --save-dev artillery
3npm install --save-dev artillery-plugin-metrics-by-endpoint1config:
2 target: 'http://localhost:4000'
3 phases:
4 - duration: 60
5 arrivalRate: 5
6 name: "Warm up"
7 - duration: 120
8 arrivalRate: 10
9 rampTo: 50
10 name: "Ramp up load"
11 - duration: 300
12 arrivalRate: 50
13 name: "Sustained load"
14 - duration: 60
15 arrivalRate: 50
16 rampTo: 5
17 name: "Ramp down"
18
19 # Global configuration
20 timeout: 30
21
22 # Load testing specific configuration
23 ensure:
24 maxErrorRate: 5 # Max 5% error rate
25 p95: 2000 # 95th percentile response time < 2000ms
26 p99: 5000 # 99th percentile response time < 5000ms
27
28 # Plugins
29 plugins:
30 metrics-by-endpoint:
31 useOnlyRequestNames: true
32
33 # Test data
34 payload:
35 path: './test-data.csv'
36 fields:
37 - 'email'
38 - 'password'
39 - 'firstName'
40 - 'lastName'
41
42 # Variables for request templates
43 variables:
44 apiVersion: 'v1'
45 userAgent: 'Artillery Load Test'
46
47# Authentication setup
48before:
49 flow:
50 - post:
51 url: "/api/{{ apiVersion }}/auth/login"
52 headers:
53 Content-Type: 'application/json'
54 User-Agent: '{{ userAgent }}'
55 json:
56 email: '[email protected]'
57 password: 'AdminPass123!'
58 capture:
59 - json: "$.token"
60 as: "authToken"
61
62scenarios:
63 # Scenario 1: User browsing and searching
64 - name: "User Browsing"
65 weight: 40
66 flow:
67 # Get product categories
68 - get:
69 url: "/api/{{ apiVersion }}/categories"
70 headers:
71 Accept: 'application/json'
72 User-Agent: '{{ userAgent }}'
73 name: "Get Categories"
74
75 # Browse products
76 - get:
77 url: "/api/{{ apiVersion }}/products"
78 qs:
79 page: "{{ $randomInt(1, 5) }}"
80 limit: 20
81 category: "{{ $randomString() }}"
82 headers:
83 Accept: 'application/json'
84 User-Agent: '{{ userAgent }}'
85 name: "Browse Products"
86
87 # Search products
88 - get:
89 url: "/api/{{ apiVersion }}/products/search"
90 qs:
91 q: "{{ $randomString() }}"
92 limit: 10
93 headers:
94 Accept: 'application/json'
95 User-Agent: '{{ userAgent }}'
96 name: "Search Products"
97
98 # View specific product
99 - get:
100 url: "/api/{{ apiVersion }}/products/{{ $randomUUID() }}"
101 headers:
102 Accept: 'application/json'
103 User-Agent: '{{ userAgent }}'
104 name: "View Product"
105 expect:
106 - statusCode: [200, 404] # Accept both success and not found
107
108 # Think time between actions
109 - think: 1
110
111 # Scenario 2: Authenticated user actions
112 - name: "Authenticated User"
113 weight: 30
114 flow:
115 # Login
116 - post:
117 url: "/api/{{ apiVersion }}/auth/login"
118 headers:
119 Content-Type: 'application/json'
120 User-Agent: '{{ userAgent }}'
121 json:
122 email: "{{ email }}"
123 password: "{{ password }}"
124 capture:
125 - json: "$.token"
126 as: "userToken"
127 name: "User Login"
128
129 # Get user profile
130 - get:
131 url: "/api/{{ apiVersion }}/users/me"
132 headers:
133 Authorization: 'Bearer {{ userToken }}'
134 Accept: 'application/json'
135 User-Agent: '{{ userAgent }}'
136 name: "Get Profile"
137
138 # Update profile
139 - put:
140 url: "/api/{{ apiVersion }}/users/me"
141 headers:
142 Authorization: 'Bearer {{ userToken }}'
143 Content-Type: 'application/json'
144 User-Agent: '{{ userAgent }}'
145 json:
146 firstName: "{{ firstName }}"
147 lastName: "{{ lastName }}"
148 name: "Update Profile"
149
150 # Get user orders
151 - get:
152 url: "/api/{{ apiVersion }}/orders"
153 headers:
154 Authorization: 'Bearer {{ userToken }}'
155 Accept: 'application/json'
156 User-Agent: '{{ userAgent }}'
157 qs:
158 limit: 10
159 name: "Get Orders"
160
161 # Think time
162 - think: 2
163
164 # Scenario 3: Admin operations
165 - name: "Admin Operations"
166 weight: 20
167 flow:
168 # Get all users (admin only)
169 - get:
170 url: "/api/{{ apiVersion }}/users"
171 headers:
172 Authorization: 'Bearer {{ authToken }}'
173 Accept: 'application/json'
174 User-Agent: '{{ userAgent }}'
175 qs:
176 page: "{{ $randomInt(1, 10) }}"
177 limit: 50
178 name: "Admin Get Users"
179
180 # Create new product
181 - post:
182 url: "/api/{{ apiVersion }}/products"
183 headers:
184 Authorization: 'Bearer {{ authToken }}'
185 Content-Type: 'application/json'
186 User-Agent: '{{ userAgent }}'
187 json:
188 name: "Load Test Product {{ $randomString() }}"
189 description: "Product created during load testing"
190 price: "{{ $randomInt(10, 1000) }}"
191 sku: "LOAD-{{ $randomString() }}"
192 categoryId: "507f1f77bcf86cd799439021"
193 name: "Create Product"
194
195 # Get product analytics
196 - get:
197 url: "/api/{{ apiVersion }}/analytics/products"
198 headers:
199 Authorization: 'Bearer {{ authToken }}'
200 Accept: 'application/json'
201 User-Agent: '{{ userAgent }}'
202 name: "Get Analytics"
203
204 # Think time
205 - think: 3
206
207 # Scenario 4: API abuse simulation
208 - name: "API Abuse"
209 weight: 10
210 flow:
211 # Rapid requests to test rate limiting
212 - loop:
213 - get:
214 url: "/api/{{ apiVersion }}/products"
215 headers:
216 Accept: 'application/json'
217 User-Agent: 'Abusive Bot'
218 name: "Rapid Requests"
219 count: 20
220
221 # Large payload request
222 - post:
223 url: "/api/{{ apiVersion }}/products"
224 headers:
225 Authorization: 'Bearer {{ authToken }}'
226 Content-Type: 'application/json'
227 User-Agent: '{{ userAgent }}'
228 json:
229 name: "{{ $randomString(1000) }}" # Very long name
230 description: "{{ $randomString(5000) }}" # Very long description
231 price: 99.99
232 sku: "LARGE-{{ $randomString() }}"
233 categoryId: "507f1f77bcf86cd799439021"
234 name: "Large Payload"
235 expect:
236 - statusCode: [201, 413, 422] # Accept success, payload too large, or validation error1const fs = require('fs');
2const path = require('path');
3
4class LoadTestRunner {
5 constructor() {
6 this.results = {};
7 this.scenarios = [];
8 }
9
10 // Generate test data
11 generateTestData() {
12 const testData = [];
13
14 for (let i = 0; i < 1000; i++) {
15 testData.push({
16 email: \`user\${i}@test.com\`,
17 password: 'TestPass123!',
18 firstName: \`User\${i}\`,
19 lastName: \`Test\${i}\`
20 });
21 }
22
23 const csvContent = [
24 'email,password,firstName,lastName',
25 ...testData.map(row => \`\${row.email},\${row.password},\${row.firstName},\${row.lastName}\`)
26 ].join('\\n');
27
28 fs.writeFileSync(path.join(__dirname, 'test-data.csv'), csvContent);
29 console.log('✅ Test data generated');
30 }
31
32 // Create stress test configuration
33 createStressTest() {
34 return {
35 config: {
36 target: 'http://localhost:4000',
37 phases: [
38 { duration: 30, arrivalRate: 1, name: "Baseline" },
39 { duration: 60, arrivalRate: 1, rampTo: 100, name: "Stress ramp-up" },
40 { duration: 120, arrivalRate: 100, name: "Stress sustained" },
41 { duration: 60, arrivalRate: 100, rampTo: 200, name: "Breaking point" },
42 { duration: 30, arrivalRate: 200, rampTo: 1, name: "Recovery" }
43 ],
44 ensure: {
45 maxErrorRate: 10,
46 p95: 5000,
47 p99: 10000
48 }
49 },
50 scenarios: [
51 {
52 name: "High Load Product Browsing",
53 weight: 80,
54 flow: [
55 {
56 get: {
57 url: "/api/v1/products",
58 qs: { page: "{{ $randomInt(1, 100) }}", limit: 50 },
59 name: "Browse Products Under Stress"
60 }
61 },
62 { think: "{{ $randomInt(0, 2) }}" }
63 ]
64 },
65 {
66 name: "Search Under Load",
67 weight: 20,
68 flow: [
69 {
70 get: {
71 url: "/api/v1/products/search",
72 qs: { q: "{{ $randomString() }}", limit: 20 },
73 name: "Search Under Stress"
74 }
75 }
76 ]
77 }
78 ]
79 };
80 }
81
82 // Create spike test configuration
83 createSpikeTest() {
84 return {
85 config: {
86 target: 'http://localhost:4000',
87 phases: [
88 { duration: 60, arrivalRate: 10, name: "Normal load" },
89 { duration: 30, arrivalRate: 10, rampTo: 500, name: "Spike up" },
90 { duration: 60, arrivalRate: 500, name: "Spike sustained" },
91 { duration: 30, arrivalRate: 500, rampTo: 10, name: "Spike down" },
92 { duration: 60, arrivalRate: 10, name: "Recovery" }
93 ],
94 ensure: {
95 maxErrorRate: 15,
96 p95: 3000
97 }
98 },
99 scenarios: [
100 {
101 name: "Spike Traffic",
102 flow: [
103 {
104 get: {
105 url: "/api/v1/products",
106 name: "Spike Request"
107 }
108 }
109 ]
110 }
111 ]
112 };
113 }
114
115 // Create endurance test configuration
116 createEnduranceTest() {
117 return {
118 config: {
119 target: 'http://localhost:4000',
120 phases: [
121 { duration: 600, arrivalRate: 50, name: "10 minute endurance" }, // 10 minutes
122 { duration: 1800, arrivalRate: 30, name: "30 minute sustained" }, // 30 minutes
123 { duration: 300, arrivalRate: 30, rampTo: 5, name: "Cool down" }
124 ],
125 ensure: {
126 maxErrorRate: 3,
127 p95: 2000,
128 p99: 4000
129 }
130 },
131 scenarios: [
132 {
133 name: "Endurance Browsing",
134 weight: 60,
135 flow: [
136 {
137 get: {
138 url: "/api/v1/products",
139 qs: { page: "{{ $randomInt(1, 20) }}" },
140 name: "Endurance Browse"
141 }
142 },
143 { think: 3 }
144 ]
145 },
146 {
147 name: "Endurance Authentication",
148 weight: 40,
149 flow: [
150 {
151 post: {
152 url: "/api/v1/auth/login",
153 json: {
154 email: "user{{ $randomInt(1, 100) }}@test.com",
155 password: "TestPass123!"
156 },
157 name: "Endurance Login"
158 }
159 },
160 { think: 5 }
161 ]
162 }
163 ]
164 };
165 }
166
167 // Run all test types
168 async runAllTests() {
169 console.log('🚀 Starting comprehensive load testing...');
170
171 // Generate test data
172 this.generateTestData();
173
174 const tests = [
175 { name: 'basic', config: require('./basic-load-test.yml') },
176 { name: 'stress', config: this.createStressTest() },
177 { name: 'spike', config: this.createSpikeTest() },
178 { name: 'endurance', config: this.createEnduranceTest() }
179 ];
180
181 for (const test of tests) {
182 console.log(\`\\n📊 Running \${test.name} test...\`);
183
184 try {
185 const result = await this.runTest(test.config);
186 this.results[test.name] = result;
187
188 console.log(\`✅ \${test.name} test completed\`);
189 this.printTestSummary(test.name, result);
190
191 } catch (error) {
192 console.error(\`❌ \${test.name} test failed:\`, error);
193 this.results[test.name] = { error: error.message };
194 }
195
196 // Wait between tests
197 if (test.name !== 'endurance') {
198 console.log('⏳ Waiting 30 seconds before next test...');
199 await this.sleep(30000);
200 }
201 }
202
203 // Generate comprehensive report
204 this.generateReport();
205 }
206
207 async runTest(config) {
208 const artillery = require('artillery');
209
210 return new Promise((resolve, reject) => {
211 const results = {
212 startTime: new Date(),
213 metrics: {},
214 errors: []
215 };
216
217 // Write config to temporary file
218 const configPath = path.join(__dirname, 'temp-config.yml');
219 fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
220
221 try {
222 artillery.run(configPath, {
223 output: path.join(__dirname, 'results', \`test-\${Date.now()}.json\`)
224 }, (report) => {
225 results.endTime = new Date();
226 results.duration = results.endTime - results.startTime;
227 results.metrics = report;
228
229 // Cleanup
230 fs.unlinkSync(configPath);
231
232 resolve(results);
233 });
234
235 } catch (error) {
236 reject(error);
237 }
238 });
239 }
240
241 printTestSummary(testName, result) {
242 if (result.error) {
243 console.log(\`❌ \${testName}: \${result.error}\`);
244 return;
245 }
246
247 const metrics = result.metrics;
248
249 console.log(\`\\n📈 \${testName.toUpperCase()} TEST SUMMARY:\`);
250 console.log(\` Duration: \${Math.round(result.duration / 1000)}s\`);
251
252 if (metrics.aggregate) {
253 const agg = metrics.aggregate;
254 console.log(\` Requests: \${agg.requestsCompleted || 0}\`);
255 console.log(\` RPS: \${agg.rps?.mean?.toFixed(2) || 'N/A'}\`);
256 console.log(\` Response time (p95): \${agg.latency?.p95 || 'N/A'}ms\`);
257 console.log(\` Response time (p99): \${agg.latency?.p99 || 'N/A'}ms\`);
258 console.log(\` Error rate: \${((agg.errors || 0) / (agg.requestsCompleted || 1) * 100).toFixed(2)}%\`);
259 }
260 }
261
262 generateReport() {
263 console.log('\\n📊 COMPREHENSIVE LOAD TEST REPORT');
264 console.log('=====================================');
265
266 Object.entries(this.results).forEach(([testName, result]) => {
267 console.log(\`\\n\${testName.toUpperCase()} TEST:\`);
268
269 if (result.error) {
270 console.log(\` Status: ❌ FAILED\`);
271 console.log(\` Error: \${result.error}\`);
272 } else {
273 const metrics = result.metrics?.aggregate;
274 if (metrics) {
275 console.log(\` Status: ✅ COMPLETED\`);
276 console.log(\` Requests: \${metrics.requestsCompleted || 0}\`);
277 console.log(\` Avg RPS: \${metrics.rps?.mean?.toFixed(2) || 'N/A'}\`);
278 console.log(\` P95 Latency: \${metrics.latency?.p95 || 'N/A'}ms\`);
279 console.log(\` Error Rate: \${((metrics.errors || 0) / (metrics.requestsCompleted || 1) * 100).toFixed(2)}%\`);
280 }
281 }
282 });
283
284 // Performance recommendations
285 console.log('\\n💡 PERFORMANCE RECOMMENDATIONS:');
286 this.generateRecommendations();
287
288 // Save detailed report
289 const reportPath = path.join(__dirname, 'results', \`load-test-report-\${Date.now()}.json\`);
290 fs.writeFileSync(reportPath, JSON.stringify({
291 timestamp: new Date(),
292 results: this.results,
293 recommendations: this.generateRecommendations()
294 }, null, 2));
295
296 console.log(\`\\n📄 Detailed report saved: \${reportPath}\`);
297 }
298
299 generateRecommendations() {
300 const recommendations = [];
301
302 Object.entries(this.results).forEach(([testName, result]) => {
303 if (result.error) {
304 recommendations.push(\`❌ \${testName}: Fix test configuration or server issues\`);
305 return;
306 }
307
308 const metrics = result.metrics?.aggregate;
309 if (!metrics) return;
310
311 const errorRate = (metrics.errors || 0) / (metrics.requestsCompleted || 1) * 100;
312 const p95Latency = metrics.latency?.p95 || 0;
313 const p99Latency = metrics.latency?.p99 || 0;
314
315 if (errorRate > 5) {
316 recommendations.push(\`⚠️ \${testName}: High error rate (\${errorRate.toFixed(2)}%) - investigate server stability\`);
317 }
318
319 if (p95Latency > 2000) {
320 recommendations.push(\`⚠️ \${testName}: Slow response times (P95: \${p95Latency}ms) - optimize performance\`);
321 }
322
323 if (p99Latency > 5000) {
324 recommendations.push(\`⚠️ \${testName}: Very slow worst-case responses (P99: \${p99Latency}ms) - check for bottlenecks\`);
325 }
326
327 if (metrics.rps?.mean < 10) {
328 recommendations.push(\`⚠️ \${testName}: Low throughput (\${metrics.rps.mean.toFixed(2)} RPS) - scale infrastructure\`);
329 }
330 });
331
332 if (recommendations.length === 0) {
333 recommendations.push('✅ All tests passed - performance looks good!');
334 }
335
336 recommendations.forEach(rec => console.log(\` \${rec}\`));
337
338 return recommendations;
339 }
340
341 sleep(ms) {
342 return new Promise(resolve => setTimeout(resolve, ms));
343 }
344}
345
346// Export for use in CI/CD
347if (require.main === module) {
348 const runner = new LoadTestRunner();
349 runner.runAllTests().catch(console.error);
350}
351
352module.exports = LoadTestRunner;Learn about Practice Exercises.
Exercise 1: Complete API Documentation Suite
Create comprehensive OpenAPI documentation for a blog API with interactive examples, authentication flows, and error handling scenarios.
Exercise 2: Comprehensive Test Coverage
Build a complete testing suite covering unit tests, integration tests, contract tests, and load tests for an e-commerce API.
Exercise 3: Load Testing Scenarios
Design and implement various load testing scenarios including stress tests, spike tests, and endurance tests with performance analysis.
Documentation:
Create comprehensive, interactive API documentation with OpenAPI
Testing Strategy:
Implement multi-layered testing approach with proper setup/teardown
Contract Testing:
Use consumer-driven contracts to ensure API compatibility
Load Testing:
Design various testing scenarios to validate performance under load
Automation:
Integrate testing into CI/CD pipelines for continuous validation
Monitoring:
Track API performance and reliability metrics continuously
You've mastered API documentation and testing strategies! You can now create comprehensive documentation, implement thorough testing suites, and ensure API reliability through various testing methodologies.
Your progress is saved in this browser, on this device only.