We use cookies to enhance your experience on the site
CodeWorlds

E2E Testing - Full Expedition Simulation

Legate! Consul Caesar.js has assigned you the most comprehensive task - conducting a full simulation of a Roman expedition from leaving the fort to returning with tributes. E2E (End-to-End) testing is like a dress rehearsal before a great expedition - we check the entire application from the user's perspective!

What Is E2E Testing?

E2E testing simulates real application usage scenarios from start to finish. It's like conducting a full Roman expedition:

  • Leaving port - user login
  • Route planning - navigating the application
  • Searching for tributes - performing business operations
  • Fighting enemies - error handling and edge cases
  • Returning to port - finalization and logout
1// Example of a full expedition E2E test
2describe('Legionary Expedition E2E', () => {
3 it('should conduct a full tribute expedition', async () => {
4 // 1. Log in as centurion
5 await request(app.getHttpServer())
6 .post('/auth/login')
7 .send({ username: 'centurion-brutus', password: 'roma123' })
8 .expect(200);
9
10 // 2. Plan the expedition
11 const expedition = await request(app.getHttpServer())
12 .post('/expeditions')
13 .send({
14 destination: 'Tribute Island',
15 legionSize: 15,
16 duration: 7,
17 expectedTributes: ['Golden Compass', 'Silver Denarii']
18 })
19 .expect(201);
20
21 // 3. Execute the expedition
22 await request(app.getHttpServer())
23 .post(`/expeditions/${expedition.body.id}/execute`)
24 .expect(200);
25
26 // 4. Check results
27 const results = await request(app.getHttpServer())
28 .get(`/expeditions/${expedition.body.id}/results`)
29 .expect(200);
30
31 expect(results.body.tributesFound).toHaveLength.toBeGreaterThan(0);
32 expect(results.body.legionStatus).toBe('healthy');
33 });
34});

E2E Configuration in NestJS

Test Environment Setup

1// test/app.e2e-spec.ts
2import { Test, TestingModule } from '@nestjs/testing';
3import { INestApplication } from '@nestjs/common';
4import * as request from 'supertest';
5import { AppModule } from './../src/app.module';
6import { TypeOrmModule } from '@nestjs/typeorm';
7import { ConfigModule } from '@nestjs/config';
8
9describe('Legionary Application E2E Tests', () => {
10 let app: INestApplication;
11 let authToken: string;
12
13 beforeAll(async () => {
14 const moduleFixture: TestingModule = await Test.createTestingModule({
15 imports: [
16 AppModule,
17 ConfigModule.forRoot({
18 envFilePath: '.env.test',
19 isGlobal: true,
20 }),
21 TypeOrmModule.forRoot({
22 type: 'postgres',
23 host: 'localhost',
24 port: 5433,
25 username: 'test_user',
26 password: 'test_pass',
27 database: 'legionaries_e2e_test',
28 autoLoadEntities: true,
29 synchronize: true,
30 dropSchema: true,
31 }),
32 ],
33 }).compile();
34
35 app = moduleFixture.createNestApplication();
36
37 // Configure the application the same way as in main.ts
38 app.useGlobalPipes(new ValidationPipe());
39 app.useGlobalFilters(new AllExceptionsFilter());
40 app.useGlobalInterceptors(new LoggingInterceptor());
41
42 await app.init();
43
44 // Seed initial data
45 await seedTestData(app);
46 });
47
48 beforeEach(async () => {
49 // Log in before each test
50 const loginResponse = await request(app.getHttpServer())
51 .post('/auth/login')
52 .send({
53 username: 'test-centurion',
54 password: 'test-password'
55 })
56 .expect(200);
57
58 authToken = loginResponse.body.access_token;
59 });
60
61 afterAll(async () => {
62 await app.close();
63 });
64});

Comprehensive E2E Scenario

1describe('Complete Legionary Workflow', () => {
2 it('should handle full legion management workflow', async () => {
3 // 1. REGISTER A NEW LEGIONARY
4 const registrationResponse = await request(app.getHttpServer())
5 .post('/auth/register')
6 .send({
7 username: 'new-legionaries-jack',
8 email: 'jack@legionaries.com',
9 password: 'strongPassword123',
10 rank: 'Legionary'
11 })
12 .expect(201);
13
14 expect(registrationResponse.body.user.username).toBe('new-legionaries-jack');
15 expect(registrationResponse.body.user.rank).toBe('Legionary');
16
17 // 2. LOG IN THE NEW LEGIONARY
18 const loginResponse = await request(app.getHttpServer())
19 .post('/auth/login')
20 .send({
21 username: 'new-legionaries-jack',
22 password: 'strongPassword123'
23 })
24 .expect(200);
25
26 const newLegionaryToken = loginResponse.body.access_token;
27 expect(newLegionaryToken).toBeDefined();
28
29 // 3. GET LEGIONARY PROFILE
30 const profileResponse = await request(app.getHttpServer())
31 .get('/profile')
32 .set('Authorization', `Bearer ${newLegionaryToken}`)
33 .expect(200);
34
35 expect(profileResponse.body.username).toBe('new-legionaries-jack');
36 expect(profileResponse.body.stats.tributesFound).toBe(0);
37 expect(profileResponse.body.stats.expeditionsCompleted).toBe(0);
38
39 // 4. JOIN A COHORT
40 const availableLegions = await request(app.getHttpServer())
41 .get('/cohorts/available')
42 .set('Authorization', `Bearer ${newLegionaryToken}`)
43 .expect(200);
44
45 expect(availableLegions.body.length).toBeGreaterThan(0);
46 const selectedLegion = availableLegions.body[0];
47
48 const joinLegionResponse = await request(app.getHttpServer())
49 .post(`/cohorts/${selectedLegion.id}/join`)
50 .set('Authorization', `Bearer ${newLegionaryToken}`)
51 .expect(200);
52
53 expect(joinLegionResponse.body.message).toContain('Successfully joined');
54
55 // 5. CHECK AVAILABLE EXPEDITIONS
56 const availableExpeditions = await request(app.getHttpServer())
57 .get('/expeditions/available')
58 .set('Authorization', `Bearer ${newLegionaryToken}`)
59 .expect(200);
60
61 expect(availableExpeditions.body.length).toBeGreaterThan(0);
62
63 // 6. SIGN UP FOR AN EXPEDITION
64 const selectedExpedition = availableExpeditions.body.find(
65 exp => exp.difficulty === 'easy' && exp.spotsAvailable > 0
66 );
67
68 const joinExpeditionResponse = await request(app.getHttpServer())
69 .post(`/expeditions/${selectedExpedition.id}/join`)
70 .set('Authorization', `Bearer ${newLegionaryToken}`)
71 .expect(200);
72
73 expect(joinExpeditionResponse.body.status).toBe('enrolled');
74
75 // 7. EXECUTE EXPEDITION (simulation)
76 const executeExpeditionResponse = await request(app.getHttpServer())
77 .post(`/expeditions/${selectedExpedition.id}/execute`)
78 .set('Authorization', `Bearer ${authToken}`) // Centurion executes the expedition
79 .expect(200);
80
81 expect(executeExpeditionResponse.body.status).toBe('completed');
82 expect(executeExpeditionResponse.body.participants).toContainEqual(
83 expect.objectContaining({ username: 'new-legionaries-jack' })
84 );
85
86 // 8. CHECK EXPEDITION RESULTS
87 const expeditionResults = await request(app.getHttpServer())
88 .get(`/expeditions/${selectedExpedition.id}/results`)
89 .set('Authorization', `Bearer ${newLegionaryToken}`)
90 .expect(200);
91
92 expect(expeditionResults.body.personalResults.tributesFound).toBeGreaterThan(0);
93 expect(expeditionResults.body.personalResults.experienceGained).toBeGreaterThan(0);
94
95 // 9. UPDATE PROFILE AFTER EXPEDITION
96 const updatedProfile = await request(app.getHttpServer())
97 .get('/profile')
98 .set('Authorization', `Bearer ${newLegionaryToken}`)
99 .expect(200);
100
101 expect(updatedProfile.body.stats.tributesFound).toBeGreaterThan(0);
102 expect(updatedProfile.body.stats.expeditionsCompleted).toBe(1);
103 expect(updatedProfile.body.experience).toBeGreaterThan(profileResponse.body.experience);
104
105 // 10. CHECK TRIBUTE INVENTORY
106 const tributeInventory = await request(app.getHttpServer())
107 .get('/tributes/my-inventory')
108 .set('Authorization', `Bearer ${newLegionaryToken}`)
109 .expect(200);
110
111 expect(tributeInventory.body.tributes.length).toBeGreaterThan(0);
112 expect(tributeInventory.body.totalValue).toBeGreaterThan(0);
113
114 // 11. TRADE A TRIBUTE
115 const firstTribute = tributeInventory.body.tributes[0];
116 const tradeResponse = await request(app.getHttpServer())
117 .post('/marketplace/sell')
118 .set('Authorization', `Bearer ${newLegionaryToken}`)
119 .send({
120 tributeId: firstTribute.id,
121 price: firstTribute.estimatedValue * 0.8
122 })
123 .expect(200);
124
125 expect(tradeResponse.body.status).toBe('listed');
126
127 // 12. CHECK LEGIONARY BALANCE
128 const walletResponse = await request(app.getHttpServer())
129 .get('/wallet')
130 .set('Authorization', `Bearer ${newLegionaryToken}`)
131 .expect(200);
132
133 expect(walletResponse.body.gold).toBeGreaterThanOrEqual(0);
134
135 // 13. PROMOTE LEGIONARY (if enough experience)
136 if (updatedProfile.body.experience >= 100) {
137 const promotionResponse = await request(app.getHttpServer())
138 .post('/profile/request-promotion')
139 .set('Authorization', `Bearer ${newLegionaryToken}`)
140 .expect(200);
141
142 expect(promotionResponse.body.status).toBe('promotion_requested');
143 }
144
145 // 14. LOG OUT
146 await request(app.getHttpServer())
147 .post('/auth/logout')
148 .set('Authorization', `Bearer ${newLegionaryToken}`)
149 .expect(200);
150
151 // 15. VERIFY TOKEN IS INVALID
152 await request(app.getHttpServer())
153 .get('/profile')
154 .set('Authorization', `Bearer ${newLegionaryToken}`)
155 .expect(401);
156 });
157});

Testing Error Scenarios

1describe('Error Scenarios E2E', () => {
2 it('should handle invalid login credentials', async () => {
3 await request(app.getHttpServer())
4 .post('/auth/login')
5 .send({
6 username: 'non-existent-legionaries',
7 password: 'wrong-password'
8 })
9 .expect(401)
10 .expect(res => {
11 expect(res.body.message).toContain('Invalid credentials');
12 });
13 });
14
15 it('should handle attempt to join a full expedition', async () => {
16 // Find an expedition that is full
17 const fullExpedition = await request(app.getHttpServer())
18 .get('/expeditions/available')
19 .set('Authorization', `Bearer ${authToken}`)
20 .expect(200)
21 .then(res => res.body.find(exp => exp.spotsAvailable === 0));
22
23 if (fullExpedition) {
24 await request(app.getHttpServer())
25 .post(`/expeditions/${fullExpedition.id}/join`)
26 .set('Authorization', `Bearer ${authToken}`)
27 .expect(409)
28 .expect(res => {
29 expect(res.body.message).toContain('expedition is full');
30 });
31 }
32 });
33
34 it('should handle unauthorized resource access attempts', async () => {
35 await request(app.getHttpServer())
36 .get('/tributes/my-inventory')
37 .expect(401);
38
39 await request(app.getHttpServer())
40 .post('/expeditions')
41 .send({ destination: 'Test Island' })
42 .expect(401);
43 });
44
45 it('should handle input data validation', async () => {
46 // Invalid registration data
47 await request(app.getHttpServer())
48 .post('/auth/register')
49 .send({
50 username: 'a', // Too short name
51 email: 'invalid-email', // Invalid email
52 password: '123', // Too weak password
53 rank: 'NonExistentRank' // Invalid rank
54 })
55 .expect(400)
56 .expect(res => {
57 expect(res.body.errors).toHaveLength.toBeGreaterThan(0);
58 });
59 });
60});

Performance E2E Testing

1describe('Performance E2E', () => {
2 it('should handle concurrent login of many legionaries', async () => {
3 const promises = [];
4 const legionariesCount = 50;
5
6 // Create 50 legionaries concurrently
7 for (let i = 0; i < legionariesCount; i++) {
8 promises.push(
9 request(app.getHttpServer())
10 .post('/auth/register')
11 .send({
12 username: `performance-legionaries-${i}`,
13 email: `legionaries${i}@legio.test`,
14 password: 'testPassword123',
15 rank: 'Legionary'
16 })
17 );
18 }
19
20 const startTime = Date.now();
21 const results = await Promise.all(promises);
22 const endTime = Date.now();
23
24 // Check that all registrations succeeded
25 results.forEach(result => {
26 expect(result.status).toBe(201);
27 });
28
29 // Check that response time is acceptable
30 const totalTime = endTime - startTime;
31 expect(totalTime).toBeLessThan(5000); // Less than 5 seconds
32
33 console.log(`${legionariesCount} legionaries registered in ${totalTime}ms`);
34 });
35
36 it('should handle a large expedition with many legionaries', async () => {
37 // Create a large expedition
38 const expeditionResponse = await request(app.getHttpServer())
39 .post('/expeditions')
40 .set('Authorization', `Bearer ${authToken}`)
41 .send({
42 destination: 'Performance Test Island',
43 maxParticipants: 100,
44 difficulty: 'medium',
45 duration: 1
46 })
47 .expect(201);
48
49 const expeditionId = expeditionResponse.body.id;
50
51 // Sign up many legionaries for the expedition
52 const joinPromises = [];
53 for (let i = 0; i < 50; i++) {
54 // Use existing test legionaries
55 joinPromises.push(
56 request(app.getHttpServer())
57 .post(`/expeditions/${expeditionId}/join`)
58 .set('Authorization', `Bearer ${getTestLegionaryToken(i)}`)
59 );
60 }
61
62 const joinResults = await Promise.allSettled(joinPromises);
63 const successfulJoins = joinResults.filter(r => r.status === 'fulfilled').length;
64
65 expect(successfulJoins).toBeGreaterThan(30); // At least 30 legionaries joined
66
67 // Execute expedition
68 const startTime = Date.now();
69 const executeResponse = await request(app.getHttpServer())
70 .post(`/expeditions/${expeditionId}/execute`)
71 .set('Authorization', `Bearer ${authToken}`)
72 .expect(200);
73 const endTime = Date.now();
74
75 expect(executeResponse.body.participants.length).toBe(successfulJoins);
76 expect(endTime - startTime).toBeLessThan(10000); // Maximum 10 seconds
77 });
78});

Helper Functions for E2E

1// test/helpers/e2e-helpers.ts
2export async function seedTestData(app: INestApplication) {
3 const dataSource = app.get(DataSource);
4
5 // Create test centurion
6 await dataSource.getRepository(User).save({
7 username: 'test-centurion',
8 email: 'centurion@legio.test',
9 password: await bcrypt.hash('test-password', 10),
10 rank: 'Centurion',
11 experience: 1000
12 });
13
14 // Create test forts
15 await dataSource.getRepository(Legion).save([
16 { name: 'Test Legion Alpha', capacity: 30, status: 'available' },
17 { name: 'Test Legion Beta', capacity: 25, status: 'available' },
18 ]);
19
20 // Create test expeditions
21 await dataSource.getRepository(Expedition).save([
22 {
23 destination: 'Test Tribute Island',
24 difficulty: 'easy',
25 maxParticipants: 20,
26 status: 'planned',
27 spotsAvailable: 15
28 },
29 {
30 destination: 'Advanced Tribute Cove',
31 difficulty: 'hard',
32 maxParticipants: 10,
33 status: 'planned',
34 spotsAvailable: 0 // Full expedition for error testing
35 }
36 ]);
37}
38
39export function getTestLegionaryToken(index: number): string {
40 // Return pre-generated token for test legionary
41 return testLegionaryTokens[index] || defaultTestToken;
42}

E2E testing is the most important test before setting out on a campaign! It checks whether your entire application works as users expect - from the first click to the last tribute!

Go to CodeWorlds