A collection of test utilities specifically designed for LoopBack 4 applications and TypeScript testing
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
OpenAPI/Swagger specification validation, JSON conversion utilities, test skipping helpers, and HTTP error logging.
OpenAPI/Swagger specification validation using the oas-validator library.
/**
* Validates OpenAPI/Swagger specifications for correctness
* @param spec - OpenAPI specification object to validate
* @throws Error if specification is invalid
*/
function validateApiSpec(spec: any): Promise<void>;Usage Examples:
import { validateApiSpec, expect } from "@loopback/testlab";
// Valid OpenAPI 3.0 specification
const validSpec = {
openapi: "3.0.0",
info: {
title: "Test API",
version: "1.0.0"
},
paths: {
"/users": {
get: {
responses: {
"200": {
description: "List of users",
content: {
"application/json": {
schema: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "string" },
name: { type: "string" }
}
}
}
}
}
}
}
}
}
}
};
// Validate specification - should not throw
await validateApiSpec(validSpec);
// Test invalid specification
const invalidSpec = {
openapi: "3.0.0",
// Missing required 'info' property
paths: {}
};
try {
await validateApiSpec(invalidSpec);
throw new Error("Should have thrown validation error");
} catch (error) {
expect(error.message).to.match(/invalid/i);
}
// Use in API testing
describe("API Specification Tests", () => {
it("should have valid OpenAPI spec", async () => {
const spec = loadApiSpecification();
await validateApiSpec(spec);
});
it("should validate generated spec", async () => {
const app = createTestApplication();
const spec = app.getApiSpecification();
await validateApiSpec(spec);
});
});Utility function for converting values to JSON-serializable format, handling edge cases.
/**
* JSON encoding utility that handles edge cases
* Converts values to JSON-serializable format by removing undefined properties
* and handling special types like Date and Function
*/
function toJSON<T>(value: T): T;
// Specific overloads for different types
function toJSON(value: Date): string;
function toJSON(value: Function): undefined;
function toJSON(value: unknown[]): unknown[];
function toJSON(value: object): object;
function toJSON(value: undefined): undefined;
function toJSON(value: null): null;
function toJSON(value: number): number;
function toJSON(value: boolean): boolean;
function toJSON(value: string): string;
// Union type overloads
function toJSON(value: unknown[] | null): unknown[] | null;
function toJSON(value: unknown[] | undefined): unknown[] | undefined;
function toJSON(value: unknown[] | null | undefined): unknown[] | null | undefined;
function toJSON(value: object | null): object | null;
function toJSON(value: object | undefined): object | undefined;
function toJSON(value: object | null | undefined): object | null | undefined;Usage Examples:
import { toJSON, expect } from "@loopback/testlab";
// Handle Date objects
const date = new Date("2023-01-01T00:00:00.000Z");
const dateJson = toJSON(date);
expect(dateJson).to.equal("2023-01-01T00:00:00.000Z");
// Handle Functions (converted to undefined)
const func = () => "hello";
const funcJson = toJSON(func);
expect(funcJson).to.be.undefined();
// Handle objects with undefined properties
const objWithUndefined = {
name: "Alice",
age: undefined,
active: true,
metadata: {
created: new Date("2023-01-01"),
updated: undefined
}
};
const cleanObj = toJSON(objWithUndefined);
// undefined properties are removed during JSON conversion
expect(cleanObj).to.not.have.property("age");
expect(cleanObj.metadata).to.not.have.property("updated");
expect(cleanObj.name).to.equal("Alice");
expect(cleanObj.active).to.be.true();
// Handle arrays
const mixedArray = [1, "hello", new Date("2023-01-01"), undefined, null];
const cleanArray = toJSON(mixedArray);
expect(cleanArray).to.eql([1, "hello", "2023-01-01T00:00:00.000Z", null, null]);
// Use in API testing for comparing expected vs actual responses
const expectedResponse = {
id: 123,
name: "Test User",
createdAt: new Date("2023-01-01"),
metadata: undefined // This will be removed
};
const apiResponse = {
id: 123,
name: "Test User",
createdAt: "2023-01-01T00:00:00.000Z"
// No metadata property
};
// Compare using toJSON to normalize
expect(toJSON(expectedResponse)).to.eql(apiResponse);
// Handle nested objects and arrays
const complexData = {
users: [
{
id: 1,
name: "Alice",
lastLogin: new Date("2023-01-01"),
preferences: undefined
},
{
id: 2,
name: "Bob",
lastLogin: new Date("2023-01-02"),
preferences: {theme: "dark", notifications: true}
}
],
metadata: {
total: 2,
generated: new Date("2023-01-03"),
cached: undefined
}
};
const normalizedData = toJSON(complexData);
expect(normalizedData.users[0]).to.not.have.property("preferences");
expect(normalizedData.users[0].lastLogin).to.be.a.String();
expect(normalizedData.metadata).to.not.have.property("cached");Helper functions for conditionally skipping tests based on various conditions.
/**
* Generic type for test definition functions (it, describe, etc.)
*/
type TestDefinition<ARGS extends unknown[], RETVAL> = (
name: string,
...args: ARGS
) => RETVAL;
/**
* Helper function for skipping tests when a certain condition is met
* @param skip - Whether to skip the test
* @param verb - Test function (it, describe) with skip capability
* @param name - Test name
* @param args - Additional test arguments
* @returns Result of calling verb or verb.skip
*/
function skipIf<ARGS extends unknown[], RETVAL>(
skip: boolean,
verb: TestDefinition<ARGS, RETVAL> & {skip: TestDefinition<ARGS, RETVAL>},
name: string,
...args: ARGS
): RETVAL;
/**
* Helper function for skipping tests on Travis CI
* @param verb - Test function (it, describe) with skip capability
* @param name - Test name
* @param args - Additional test arguments
* @returns Result of calling verb or verb.skip if on Travis
*/
function skipOnTravis<ARGS extends unknown[], RETVAL>(
verb: TestDefinition<ARGS, RETVAL> & {skip: TestDefinition<ARGS, RETVAL>},
name: string,
...args: ARGS
): RETVAL;Usage Examples:
import { skipIf, skipOnTravis } from "@loopback/testlab";
// Skip based on feature flags
const features = {
freeFormProperties: process.env.ENABLE_FREEFORM === "true",
advancedAuth: process.env.ENABLE_ADVANCED_AUTH === "true",
experimentalFeatures: process.env.NODE_ENV === "development"
};
// Skip test suite if feature not enabled
skipIf(
!features.freeFormProperties,
describe,
"free-form properties (strict: false)",
() => {
it("should allow arbitrary properties", () => {
// Test implementation
});
it("should validate known properties", () => {
// Test implementation
});
}
);
// Skip individual test based on condition
skipIf(
!features.advancedAuth,
it,
"should support multi-factor authentication",
async () => {
// Test implementation for MFA
}
);
// Skip based on environment
skipIf(
process.platform === "win32",
it,
"should handle Unix file permissions",
() => {
// Unix-specific test
}
);
// Skip on Travis CI (for tests that don't work well in CI)
skipOnTravis(it, "should connect to external service", async () => {
// Test that requires external network access
});
skipOnTravis(describe, "Integration tests with external APIs", () => {
it("should fetch data from API", async () => {
// External API test
});
});
// Complex conditional skipping
const isCI = process.env.CI === "true";
const hasDockerAccess = process.env.DOCKER_AVAILABLE === "true";
skipIf(
isCI && !hasDockerAccess,
describe,
"Docker integration tests",
() => {
it("should start container", () => {
// Docker test
});
}
);
// Skip based on Node.js version
const nodeVersion = process.version;
const isOldNode = parseInt(nodeVersion.slice(1)) < 14;
skipIf(
isOldNode,
it,
"should use modern JavaScript features",
() => {
// Test using features from Node 14+
}
);
// Dynamic test generation with conditional skipping
const testCases = [
{name: "basic", condition: true},
{name: "advanced", condition: features.advancedAuth},
{name: "experimental", condition: features.experimentalFeatures}
];
testCases.forEach(testCase => {
skipIf(
!testCase.condition,
it,
`should handle ${testCase.name} scenario`,
() => {
// Test implementation
}
);
});Additional utilities for test environment detection and management.
Usage Examples:
import { skipIf, skipOnTravis } from "@loopback/testlab";
// Environment detection helpers
const isCI = process.env.CI === "true";
const isTravis = process.env.TRAVIS === "true";
const isGitHubActions = process.env.GITHUB_ACTIONS === "true";
const isLocal = !isCI;
// Database availability
const hasDatabase = process.env.DATABASE_URL !== undefined;
const hasRedis = process.env.REDIS_URL !== undefined;
// External service availability
const hasS3Access = process.env.AWS_ACCESS_KEY_ID !== undefined;
const hasEmailService = process.env.SMTP_HOST !== undefined;
// Skip tests based on service availability
skipIf(!hasDatabase, describe, "Database integration tests", () => {
it("should connect to database", async () => {
// Database test
});
});
skipIf(!hasRedis, it, "should cache data in Redis", async () => {
// Redis test
});
// Skip flaky tests in CI
skipIf(isCI, it, "should handle timing-sensitive operations", async () => {
// Test that might be flaky in CI due to timing
});
// Skip tests requiring manual setup
skipIf(!hasS3Access, describe, "S3 file upload tests", () => {
it("should upload file to S3", async () => {
// S3 upload test
});
});
// Conditional test suites for different environments
if (isLocal) {
describe("Local development tests", () => {
it("should use development configuration", () => {
// Development-specific tests
});
});
}
if (isCI) {
describe("CI-specific tests", () => {
it("should use production-like configuration", () => {
// CI-specific tests
});
});
}
// Platform-specific tests
const testsByPlatform = {
linux: () => {
it("should handle Linux-specific features", () => {
// Linux test
});
},
darwin: () => {
it("should handle macOS-specific features", () => {
// macOS test
});
},
win32: () => {
it("should handle Windows-specific features", () => {
// Windows test
});
}
};
const currentPlatform = process.platform as keyof typeof testsByPlatform;
if (testsByPlatform[currentPlatform]) {
describe(`${currentPlatform} platform tests`, testsByPlatform[currentPlatform]);
}
// Version-based skipping
const nodeVersion = parseInt(process.version.slice(1));
skipIf(nodeVersion < 16, it, "should use Node 16+ features", () => {
// Modern Node.js features
});
// Memory and performance tests
const hasEnoughMemory = process.env.MEMORY_TEST === "true";
skipIf(!hasEnoughMemory, it, "should handle large datasets", () => {
// Memory-intensive test
});
// Integration with test frameworks
describe("Conditional test examples", () => {
// Standard test
it("should always run", () => {
expect(true).to.be.true();
});
// Conditionally skipped test
skipIf(
process.env.SKIP_SLOW_TESTS === "true",
it,
"should complete slow operation",
async function() {
this.timeout(30000); // 30 second timeout
// Slow test implementation
}
);
// Travis-specific skip
skipOnTravis(it, "should work with external dependencies", () => {
// Test that doesn't work well on Travis
});
});