0
# Validation and Helpers
1
2
OpenAPI/Swagger specification validation, JSON conversion utilities, test skipping helpers, and HTTP error logging.
3
4
## Capabilities
5
6
### API Specification Validation
7
8
OpenAPI/Swagger specification validation using the oas-validator library.
9
10
```typescript { .api }
11
/**
12
* Validates OpenAPI/Swagger specifications for correctness
13
* @param spec - OpenAPI specification object to validate
14
* @throws Error if specification is invalid
15
*/
16
function validateApiSpec(spec: any): Promise<void>;
17
```
18
19
**Usage Examples:**
20
21
```typescript
22
import { validateApiSpec, expect } from "@loopback/testlab";
23
24
// Valid OpenAPI 3.0 specification
25
const validSpec = {
26
openapi: "3.0.0",
27
info: {
28
title: "Test API",
29
version: "1.0.0"
30
},
31
paths: {
32
"/users": {
33
get: {
34
responses: {
35
"200": {
36
description: "List of users",
37
content: {
38
"application/json": {
39
schema: {
40
type: "array",
41
items: {
42
type: "object",
43
properties: {
44
id: { type: "string" },
45
name: { type: "string" }
46
}
47
}
48
}
49
}
50
}
51
}
52
}
53
}
54
}
55
}
56
};
57
58
// Validate specification - should not throw
59
await validateApiSpec(validSpec);
60
61
// Test invalid specification
62
const invalidSpec = {
63
openapi: "3.0.0",
64
// Missing required 'info' property
65
paths: {}
66
};
67
68
try {
69
await validateApiSpec(invalidSpec);
70
throw new Error("Should have thrown validation error");
71
} catch (error) {
72
expect(error.message).to.match(/invalid/i);
73
}
74
75
// Use in API testing
76
describe("API Specification Tests", () => {
77
it("should have valid OpenAPI spec", async () => {
78
const spec = loadApiSpecification();
79
await validateApiSpec(spec);
80
});
81
82
it("should validate generated spec", async () => {
83
const app = createTestApplication();
84
const spec = app.getApiSpecification();
85
await validateApiSpec(spec);
86
});
87
});
88
```
89
90
### JSON Conversion
91
92
Utility function for converting values to JSON-serializable format, handling edge cases.
93
94
```typescript { .api }
95
/**
96
* JSON encoding utility that handles edge cases
97
* Converts values to JSON-serializable format by removing undefined properties
98
* and handling special types like Date and Function
99
*/
100
function toJSON<T>(value: T): T;
101
102
// Specific overloads for different types
103
function toJSON(value: Date): string;
104
function toJSON(value: Function): undefined;
105
function toJSON(value: unknown[]): unknown[];
106
function toJSON(value: object): object;
107
function toJSON(value: undefined): undefined;
108
function toJSON(value: null): null;
109
function toJSON(value: number): number;
110
function toJSON(value: boolean): boolean;
111
function toJSON(value: string): string;
112
113
// Union type overloads
114
function toJSON(value: unknown[] | null): unknown[] | null;
115
function toJSON(value: unknown[] | undefined): unknown[] | undefined;
116
function toJSON(value: unknown[] | null | undefined): unknown[] | null | undefined;
117
function toJSON(value: object | null): object | null;
118
function toJSON(value: object | undefined): object | undefined;
119
function toJSON(value: object | null | undefined): object | null | undefined;
120
```
121
122
**Usage Examples:**
123
124
```typescript
125
import { toJSON, expect } from "@loopback/testlab";
126
127
// Handle Date objects
128
const date = new Date("2023-01-01T00:00:00.000Z");
129
const dateJson = toJSON(date);
130
expect(dateJson).to.equal("2023-01-01T00:00:00.000Z");
131
132
// Handle Functions (converted to undefined)
133
const func = () => "hello";
134
const funcJson = toJSON(func);
135
expect(funcJson).to.be.undefined();
136
137
// Handle objects with undefined properties
138
const objWithUndefined = {
139
name: "Alice",
140
age: undefined,
141
active: true,
142
metadata: {
143
created: new Date("2023-01-01"),
144
updated: undefined
145
}
146
};
147
148
const cleanObj = toJSON(objWithUndefined);
149
// undefined properties are removed during JSON conversion
150
expect(cleanObj).to.not.have.property("age");
151
expect(cleanObj.metadata).to.not.have.property("updated");
152
expect(cleanObj.name).to.equal("Alice");
153
expect(cleanObj.active).to.be.true();
154
155
// Handle arrays
156
const mixedArray = [1, "hello", new Date("2023-01-01"), undefined, null];
157
const cleanArray = toJSON(mixedArray);
158
expect(cleanArray).to.eql([1, "hello", "2023-01-01T00:00:00.000Z", null, null]);
159
160
// Use in API testing for comparing expected vs actual responses
161
const expectedResponse = {
162
id: 123,
163
name: "Test User",
164
createdAt: new Date("2023-01-01"),
165
metadata: undefined // This will be removed
166
};
167
168
const apiResponse = {
169
id: 123,
170
name: "Test User",
171
createdAt: "2023-01-01T00:00:00.000Z"
172
// No metadata property
173
};
174
175
// Compare using toJSON to normalize
176
expect(toJSON(expectedResponse)).to.eql(apiResponse);
177
178
// Handle nested objects and arrays
179
const complexData = {
180
users: [
181
{
182
id: 1,
183
name: "Alice",
184
lastLogin: new Date("2023-01-01"),
185
preferences: undefined
186
},
187
{
188
id: 2,
189
name: "Bob",
190
lastLogin: new Date("2023-01-02"),
191
preferences: {theme: "dark", notifications: true}
192
}
193
],
194
metadata: {
195
total: 2,
196
generated: new Date("2023-01-03"),
197
cached: undefined
198
}
199
};
200
201
const normalizedData = toJSON(complexData);
202
expect(normalizedData.users[0]).to.not.have.property("preferences");
203
expect(normalizedData.users[0].lastLogin).to.be.a.String();
204
expect(normalizedData.metadata).to.not.have.property("cached");
205
```
206
207
### Test Skipping Utilities
208
209
Helper functions for conditionally skipping tests based on various conditions.
210
211
```typescript { .api }
212
/**
213
* Generic type for test definition functions (it, describe, etc.)
214
*/
215
type TestDefinition<ARGS extends unknown[], RETVAL> = (
216
name: string,
217
...args: ARGS
218
) => RETVAL;
219
220
/**
221
* Helper function for skipping tests when a certain condition is met
222
* @param skip - Whether to skip the test
223
* @param verb - Test function (it, describe) with skip capability
224
* @param name - Test name
225
* @param args - Additional test arguments
226
* @returns Result of calling verb or verb.skip
227
*/
228
function skipIf<ARGS extends unknown[], RETVAL>(
229
skip: boolean,
230
verb: TestDefinition<ARGS, RETVAL> & {skip: TestDefinition<ARGS, RETVAL>},
231
name: string,
232
...args: ARGS
233
): RETVAL;
234
235
/**
236
* Helper function for skipping tests on Travis CI
237
* @param verb - Test function (it, describe) with skip capability
238
* @param name - Test name
239
* @param args - Additional test arguments
240
* @returns Result of calling verb or verb.skip if on Travis
241
*/
242
function skipOnTravis<ARGS extends unknown[], RETVAL>(
243
verb: TestDefinition<ARGS, RETVAL> & {skip: TestDefinition<ARGS, RETVAL>},
244
name: string,
245
...args: ARGS
246
): RETVAL;
247
```
248
249
**Usage Examples:**
250
251
```typescript
252
import { skipIf, skipOnTravis } from "@loopback/testlab";
253
254
// Skip based on feature flags
255
const features = {
256
freeFormProperties: process.env.ENABLE_FREEFORM === "true",
257
advancedAuth: process.env.ENABLE_ADVANCED_AUTH === "true",
258
experimentalFeatures: process.env.NODE_ENV === "development"
259
};
260
261
// Skip test suite if feature not enabled
262
skipIf(
263
!features.freeFormProperties,
264
describe,
265
"free-form properties (strict: false)",
266
() => {
267
it("should allow arbitrary properties", () => {
268
// Test implementation
269
});
270
271
it("should validate known properties", () => {
272
// Test implementation
273
});
274
}
275
);
276
277
// Skip individual test based on condition
278
skipIf(
279
!features.advancedAuth,
280
it,
281
"should support multi-factor authentication",
282
async () => {
283
// Test implementation for MFA
284
}
285
);
286
287
// Skip based on environment
288
skipIf(
289
process.platform === "win32",
290
it,
291
"should handle Unix file permissions",
292
() => {
293
// Unix-specific test
294
}
295
);
296
297
// Skip on Travis CI (for tests that don't work well in CI)
298
skipOnTravis(it, "should connect to external service", async () => {
299
// Test that requires external network access
300
});
301
302
skipOnTravis(describe, "Integration tests with external APIs", () => {
303
it("should fetch data from API", async () => {
304
// External API test
305
});
306
});
307
308
// Complex conditional skipping
309
const isCI = process.env.CI === "true";
310
const hasDockerAccess = process.env.DOCKER_AVAILABLE === "true";
311
312
skipIf(
313
isCI && !hasDockerAccess,
314
describe,
315
"Docker integration tests",
316
() => {
317
it("should start container", () => {
318
// Docker test
319
});
320
}
321
);
322
323
// Skip based on Node.js version
324
const nodeVersion = process.version;
325
const isOldNode = parseInt(nodeVersion.slice(1)) < 14;
326
327
skipIf(
328
isOldNode,
329
it,
330
"should use modern JavaScript features",
331
() => {
332
// Test using features from Node 14+
333
}
334
);
335
336
// Dynamic test generation with conditional skipping
337
const testCases = [
338
{name: "basic", condition: true},
339
{name: "advanced", condition: features.advancedAuth},
340
{name: "experimental", condition: features.experimentalFeatures}
341
];
342
343
testCases.forEach(testCase => {
344
skipIf(
345
!testCase.condition,
346
it,
347
`should handle ${testCase.name} scenario`,
348
() => {
349
// Test implementation
350
}
351
);
352
});
353
```
354
355
### Environment Detection and Helpers
356
357
Additional utilities for test environment detection and management.
358
359
**Usage Examples:**
360
361
```typescript
362
import { skipIf, skipOnTravis } from "@loopback/testlab";
363
364
// Environment detection helpers
365
const isCI = process.env.CI === "true";
366
const isTravis = process.env.TRAVIS === "true";
367
const isGitHubActions = process.env.GITHUB_ACTIONS === "true";
368
const isLocal = !isCI;
369
370
// Database availability
371
const hasDatabase = process.env.DATABASE_URL !== undefined;
372
const hasRedis = process.env.REDIS_URL !== undefined;
373
374
// External service availability
375
const hasS3Access = process.env.AWS_ACCESS_KEY_ID !== undefined;
376
const hasEmailService = process.env.SMTP_HOST !== undefined;
377
378
// Skip tests based on service availability
379
skipIf(!hasDatabase, describe, "Database integration tests", () => {
380
it("should connect to database", async () => {
381
// Database test
382
});
383
});
384
385
skipIf(!hasRedis, it, "should cache data in Redis", async () => {
386
// Redis test
387
});
388
389
// Skip flaky tests in CI
390
skipIf(isCI, it, "should handle timing-sensitive operations", async () => {
391
// Test that might be flaky in CI due to timing
392
});
393
394
// Skip tests requiring manual setup
395
skipIf(!hasS3Access, describe, "S3 file upload tests", () => {
396
it("should upload file to S3", async () => {
397
// S3 upload test
398
});
399
});
400
401
// Conditional test suites for different environments
402
if (isLocal) {
403
describe("Local development tests", () => {
404
it("should use development configuration", () => {
405
// Development-specific tests
406
});
407
});
408
}
409
410
if (isCI) {
411
describe("CI-specific tests", () => {
412
it("should use production-like configuration", () => {
413
// CI-specific tests
414
});
415
});
416
}
417
418
// Platform-specific tests
419
const testsByPlatform = {
420
linux: () => {
421
it("should handle Linux-specific features", () => {
422
// Linux test
423
});
424
},
425
darwin: () => {
426
it("should handle macOS-specific features", () => {
427
// macOS test
428
});
429
},
430
win32: () => {
431
it("should handle Windows-specific features", () => {
432
// Windows test
433
});
434
}
435
};
436
437
const currentPlatform = process.platform as keyof typeof testsByPlatform;
438
if (testsByPlatform[currentPlatform]) {
439
describe(`${currentPlatform} platform tests`, testsByPlatform[currentPlatform]);
440
}
441
442
// Version-based skipping
443
const nodeVersion = parseInt(process.version.slice(1));
444
skipIf(nodeVersion < 16, it, "should use Node 16+ features", () => {
445
// Modern Node.js features
446
});
447
448
// Memory and performance tests
449
const hasEnoughMemory = process.env.MEMORY_TEST === "true";
450
skipIf(!hasEnoughMemory, it, "should handle large datasets", () => {
451
// Memory-intensive test
452
});
453
454
// Integration with test frameworks
455
describe("Conditional test examples", () => {
456
// Standard test
457
it("should always run", () => {
458
expect(true).to.be.true();
459
});
460
461
// Conditionally skipped test
462
skipIf(
463
process.env.SKIP_SLOW_TESTS === "true",
464
it,
465
"should complete slow operation",
466
async function() {
467
this.timeout(30000); // 30 second timeout
468
// Slow test implementation
469
}
470
);
471
472
// Travis-specific skip
473
skipOnTravis(it, "should work with external dependencies", () => {
474
// Test that doesn't work well on Travis
475
});
476
});
477
```