Integration Test Patterns
Integration tests verify real API interactions with live credentials. They test authentication, network communication, and API behavior.
🚨 CRITICAL RULES (Immediate Failure)
1. Test Data Configuration - NEVER Hardcode Values
🚨 CRITICAL: ALL integration test values MUST be in .env, NEVER hardcoded
When user provides test values (IDs, names, etc.), they MUST go in .env:
# ✅ CORRECT - Test values in .env SERVICE_API_KEY=your-api-key SERVICE_TEST_USER_ID=12345 SERVICE_TEST_ORGANIZATION_ID=1067 SERVICE_TEST_RESOURCE_NAME=test-resource # ❌ WRONG - Hardcoding in test files const userId = '12345'; // NO! const orgId = '1067'; // NO!
RULE:
- •User gives you test values → Add to
.envimmediately - •Export from
test/integration/Common.ts - •Import and use in integration tests
- •NEVER hardcode any test data values
Example:
// .env
AVIGILON_ALTA_ACCESS_TEST_ORGANIZATION_ID=1067
// test/integration/Common.ts
export const AVIGILON_ALTA_ACCESS_TEST_ORGANIZATION_ID =
process.env.AVIGILON_ALTA_ACCESS_TEST_ORGANIZATION_ID || '';
// test/integration/OrganizationProducerTest.ts
import { AVIGILON_ALTA_ACCESS_TEST_ORGANIZATION_ID } from './Common';
it('should retrieve organization', async () => {
const organizationId = AVIGILON_ALTA_ACCESS_TEST_ORGANIZATION_ID; // ✅ From .env
// NOT: const organizationId = '1067'; // ❌ Hardcoded
const org = await api.get(organizationId);
expect(org.id).to.equal(organizationId);
});
WHY:
- •Test values are configuration, not code
- •Different developers may need different test IDs
- •Makes tests portable across environments
- •Easy to update without touching code
- •Consistent with credential management
2. Credential Loading for Integration Tests
🚨 CRITICAL: Automatic credential loading with dotenv
Integration tests need credentials and test values from .env file. Configure automatic loading:
Step 0: Create .env file after scaffolding
IMMEDIATELY after running module scaffolding, create a .env file in the module root with credentials from the user's initial request:
# Example: User said "credentials are in .env: SERVICE_EMAIL and SERVICE_PASSWORD" # Create module/.env with: SERVICE_EMAIL=user@example.com SERVICE_PASSWORD=their-password SERVICE_API_KEY=their-api-key
Important: Extract credentials from wherever user specified in their request:
- •Module root
.env - •Repository root
.env - •
.connectionProfile.json - •User message directly
- •System environment
Step 1: Install dotenv
npm install --save-dev dotenv
Step 2: Configure .mocharc.json
{
"extension": ["ts"],
"require": ["ts-node/register", "dotenv/config"]
}
Step 3: Load dotenv explicitly in test/integration/Common.ts
// test/integration/Common.ts - ONLY file allowed to access process.env
import { config } from 'dotenv';
// Load .env file explicitly to ensure credentials are available
config();
export const SERVICE_API_KEY = process.env.SERVICE_API_KEY || '';
export const SERVICE_BASE_URL = process.env.SERVICE_BASE_URL || 'https://api.example.com';
export function hasCredentials(): boolean {
return !!SERVICE_API_KEY;
}
Why explicit config()?
- •.mocharc.json
"require": ["dotenv/config"]loads dotenv, BUT - •Environment variables in Common.ts are evaluated at module load time
- •Explicit
config()ensures .env is loaded BEFORE env vars are accessed - •This guarantees integration tests can detect credentials properly
Step 4: Use in integration tests
import { hasCredentials, SERVICE_API_KEY } from './Common';
describe('Service Integration Tests', function () {
before(function () {
if (!hasCredentials()) {
this.skip(); // Skip entire suite if no credentials
}
});
it('should connect with real API', async function () {
// Test with real credentials
});
});
Benefits:
- •✅ Credentials automatically loaded from
.env - •✅ No manual process.env access in test files
- •✅ Graceful skipping when credentials unavailable
- •✅ Consistent credential management across all modules
Document in USERGUIDE.md:
### Setting Up Credentials for Testing Create a `.env` file in the module root: \`\`\`bash SERVICE_API_KEY=your-api-key SERVICE_SECRET=your-secret \`\`\` The test suite automatically loads credentials using `dotenv`.
3. Debug Logging is MANDATORY
🚨 CRITICAL: All integration tests MUST include debug logging
import { getLogger, hasCredentials } from './Common';
const logger = getLogger('{Resource}ProducerTest');
describe('{Resource} Producer Integration', function () {
before(function () {
if (!hasCredentials()) {
this.skip();
}
});
it('should perform operation with real API', async function () {
const connector = newService();
logger.debug('connector.connect(email, password)');
await connector.connect({ email, password });
logger.debug('→', JSON.stringify(connectionState, null, 2));
const api = connector.getResourceApi();
logger.debug('api.getResource(id)');
const result = await api.getResource('123');
logger.debug('→', JSON.stringify(result, null, 2));
expect(result).to.have.property('id');
});
});
Debug Logging Requirements:
- •✅ Import getLogger from
./Common(not from @zerobias-org/logger directly) - •✅ Create logger with test file name:
const logger = getLogger('{Resource}ProducerTest') - •✅ Log operation call BEFORE execution:
logger.debug('api.methodName(param1, param2)') - •✅ Log result AFTER execution:
logger.debug('→', JSON.stringify(result, null, 2)) - •✅ Use arrow
→to indicate result - •✅ Always use
JSON.stringify(result, null, 2)for consistent formatting
Why debug logging?
- •Visible when log level set to debug via
LOG_LEVEL=debug npm run test:integration - •Shows exact operation calls with parameters
- •Shows complete API responses for debugging
- •Helps diagnose integration issues without re-running tests
🟡 STANDARD RULES
Test File Naming Convention
MANDATORY naming pattern for integration test files:
test/integration/{Resource}ProducerTest.ts # Producer integration tests
test/integration/ConnectionTest.ts # Connection integration tests
Examples:
- •✅
test/integration/AccessProducerTest.ts- integration test - •✅
test/integration/UserProducerTest.ts- integration test - •✅
test/integration/ConnectionTest.ts- integration test
Rationale:
- •Consistent naming makes test files easy to find
- •Integration tests use SAME names as unit tests - folder location distinguishes them
- •No need for
IntegrationTestsuffix sincetest/integration/folder makes it clear
Connection Testing - Integration Tests
EVERY module MUST have connection integration tests: test/integration/ConnectionTest.ts
Tests with real API and credentials.
// test/integration/ConnectionTest.ts
import { expect } from 'chai';
import { LoggerEngine } from '@zerobias-org/logger';
import { Email } from '@zerobias-org/types-core-js';
import { newService } from '../../src';
import { SERVICE_EMAIL, SERVICE_PASSWORD, hasCredentials } from './Common';
const logger = LoggerEngine.root().get('ConnectionTest');
describe('Connection Integration Tests', function () {
this.timeout(30000);
before(function () {
if (!hasCredentials()) {
this.skip();
}
});
describe('connect', () => {
it('should successfully connect to real API', async () => {
const connector = newService();
logger.debug('connector.connect(email, password)');
const connectionState = await connector.connect({
email: new Email(SERVICE_EMAIL),
password: SERVICE_PASSWORD,
});
logger.debug('→', JSON.stringify(connectionState, null, 2));
expect(connectionState).to.have.property('accessToken');
});
it('should fail with invalid credentials', async () => {
const connector = newService();
logger.debug('connector.connect(invalid credentials)');
try {
await connector.connect({
email: new Email('invalid@example.com'),
password: 'wrongpassword',
});
expect.fail('Should have thrown an error');
} catch (error: any) {
logger.debug('→ error', JSON.stringify({ name: error.name }, null, 2));
expect(error).to.exist;
}
});
});
describe('isConnected', () => {
it('should return true when connected', async () => {
const connector = newService();
await connector.connect({
email: new Email(SERVICE_EMAIL),
password: SERVICE_PASSWORD,
});
logger.debug('connector.isConnected()');
const isConnected = await connector.isConnected();
logger.debug('→', JSON.stringify({ isConnected }, null, 2));
expect(isConnected).to.be.true;
});
it('should return false when not connected', async () => {
const connector = newService();
logger.debug('connector.isConnected() - without connection');
const isConnected = await connector.isConnected();
logger.debug('→', JSON.stringify({ isConnected }, null, 2));
expect(isConnected).to.be.false;
});
});
describe('disconnect', () => {
it('should successfully disconnect from API', async () => {
const connector = newService();
await connector.connect({
email: new Email(SERVICE_EMAIL),
password: SERVICE_PASSWORD,
});
logger.debug('connector.disconnect()');
await connector.disconnect();
logger.debug('→ disconnect completed');
});
});
});
Benefits:
- •✅ Tests real API authentication and connection
- •✅ Verifies credentials work correctly
- •✅ Catches network and API-specific issues
- •✅ Debug logging shows actual API responses
Connected Instance Helpers - Integration Test Common.ts
CRITICAL: Integration test Common.ts provides real credentials with connection caching
// test/integration/Common.ts
import { config } from 'dotenv';
import { Email } from '@zerobias-org/types-core-js';
import { LoggerEngine } from '@zerobias-org/logger';
import { newService } from '../../src';
import type { ServiceConnector } from '../../src';
// Load .env file explicitly to ensure credentials are available
config();
export const SERVICE_EMAIL = process.env.SERVICE_EMAIL || '';
export const SERVICE_PASSWORD = process.env.SERVICE_PASSWORD || '';
// Test data values - export any test IDs, names, or other values from .env
export const SERVICE_TEST_USER_ID = process.env.SERVICE_TEST_USER_ID || '';
export const SERVICE_TEST_ORGANIZATION_ID = process.env.SERVICE_TEST_ORGANIZATION_ID || '';
/**
* Get a logger with configurable level from LOG_LEVEL env var.
* Usage: LOG_LEVEL=debug npm run test:integration
*/
export function getLogger(name: string) {
return LoggerEngine.root().get(name);
}
if (process.env.LOG_LEVEL) {
switch (process.env.LOG_LEVEL) {
case 'trace': {
getLogger().setLevel(LogLevel.TRACE);
break;
}
case 'debug': {
getLogger().setLevel(LogLevel.DEBUG);
break;
}
case 'verbose': {
getLogger().setLevel(LogLevel.VERBOSE);
break;
}
case 'info': {
getLogger().setLevel(LogLevel.INFO);
break;
}
case 'warn': {
getLogger().setLevel(LogLevel.WARN);
break;
}
case 'error': {
getLogger().setLevel(LogLevel.ERROR);
break;
}
case 'crit': {
getLogger().setLevel(LogLevel.CRIT);
break;
}
default: {
getLogger().setLevel(LogLevel.INFO);
break;
}
}
}
export function hasCredentials(): boolean {
return !!SERVICE_EMAIL && !!SERVICE_PASSWORD;
}
// Cached connector instance - connect once, reuse many times
let cachedConnector: ServiceConnector | null = null;
/**
* Get a connected instance for integration testing.
* Connects once on first call, then returns cached instance on subsequent calls.
* This avoids repeated authentication overhead across multiple tests.
* Uses real credentials from .env file.
* Makes real API calls.
*/
export async function getConnectedInstance(): Promise<ServiceConnector> {
if (cachedConnector) {
return cachedConnector;
}
const connector = newService();
await connector.connect({
email: new Email(SERVICE_EMAIL),
password: SERVICE_PASSWORD,
});
cachedConnector = connector;
return connector;
}
Key Points:
- •✅ Uses dotenv to load real credentials
- •✅ Exports credential constants
- •✅ Exports getLogger() helper that respects LOG_LEVEL env var
- •✅ Has hasCredentials() check
- •✅ Connection caching: Connects once on first call, returns cached instance on subsequent calls
- •✅ Performance: Avoids repeated authentication overhead across multiple integration tests
- •✅ Makes real API calls
- •✅ Requires .env file to run
- •✅ Run with
LOG_LEVEL=debug npm run test:integrationto see debug logs
Usage in integration tests:
// test/integration/ServiceProducerTest.ts
import { expect } from 'chai';
import { getConnectedInstance, hasCredentials, getLogger } from './Common';
const logger = getLogger('ServiceProducerTest');
describe('Service Integration', function () {
before(function () {
if (!hasCredentials()) {
this.skip(); // Skip if no credentials
}
});
it('should retrieve access token info from real API', async () => {
logger.debug('getConnectedInstance()');
const connector = await getConnectedInstance();
const api = connector.getAccessApi();
logger.debug('api.getToken()');
const tokenInfo = await api.getToken();
logger.debug('→', JSON.stringify(tokenInfo, null, 2));
expect(tokenInfo.token).to.exist;
});
});
Environment Variable Usage
CRITICAL: ONLY integration tests use environment variables
- •ONLY ALLOWED IN:
test/integration/Common.ts - •NEVER in: Test files or unit tests
- •Test files: Import from Common.ts, NEVER access process.env directly
// ✅ CORRECT: test/integration/Common.ts ONLY
import { config } from 'dotenv';
config();
export const TEST_API_KEY = process.env.TEST_API_KEY || '';
export const TEST_BASE_URL = process.env.TEST_BASE_URL || 'https://api.example.com';
export function hasCredentials(): boolean {
return !!TEST_API_KEY;
}
// ✅ CORRECT: Integration test files
import { TEST_API_KEY, hasCredentials } from './Common';
// ❌ WRONG: Direct access in test files
const apiKey = process.env.TEST_API_KEY;
File Organization
Integration test structure:
test/
├── integration/
│ ├── Common.ts # Real credentials from .env + getConnectedInstance
│ ├── ConnectionTest.ts # Real API calls, requires .env
│ └── {Resource}ProducerTest.ts # Real API calls, requires .env
└── .env # Credentials (not committed)
Key points:
- •✅
test/integration/Common.tsloads real credentials from .env - •✅ Has hasCredentials() check
- •✅ Import from
./Common(same folder) - •✅ Integration tests skip if no credentials via hasCredentials() check
Integration Test Pattern
Standard integration test structure with debug logging:
import { getLogger, hasCredentials } from './Common';
const logger = getLogger('{Resource}ProducerTest');
describe('{Resource} Producer Integration', function () {
// Set timeout for real API calls
this.timeout(30000);
before(function () {
// Skip entire suite if no credentials
if (!hasCredentials()) {
this.skip(); // This is OK - skips whole suite
}
});
it('should perform operation with real API', async function () {
const connector = newService();
logger.debug('connector.connect(email, password)');
await connector.connect({ email, password });
logger.debug('→', JSON.stringify(connectionState, null, 2));
const api = connector.getResourceApi();
logger.debug('api.getResource(id)');
const result = await api.getResource('123');
logger.debug('→', JSON.stringify(result, null, 2));
expect(result).to.have.property('id');
});
});
Key elements:
- •✅ Import getLogger from ./Common
- •✅ Set timeout for real API calls
- •✅ Skip suite if no credentials (in before() hook)
- •✅ Debug logging BEFORE and AFTER each operation
- •✅ Use arrow
→to indicate result - •✅ JSON.stringify for consistent formatting
Note: Using this.skip() in before() hook is ALLOWED - it gracefully skips the entire suite when credentials missing.
Test Data Management
Use real API responses:
it('should retrieve organization', async () => {
logger.debug('api.getOrganization(id)');
const org = await api.getOrganization(TEST_ORGANIZATION_ID);
logger.debug('→', JSON.stringify(org, null, 2));
// Verify real API response structure
expect(org).to.have.property('id');
expect(org).to.have.property('name');
expect(org.id).to.be.instanceof(UUID);
});
Rules:
- •✅ Use real API responses
- •✅ Load test IDs from .env via Common.ts
- •✅ Debug logging shows actual API responses
- •✅ Verify response structure and types
Credential Discovery
Where to find credentials (in order):
- •Check
.envfile first - •Check
.connectionProfile.jsonsecond - •Ask user if not found
- •Skip integration tests gracefully if no credentials
// test/integration/Common.ts
export function hasCredentials(): boolean {
return !!SERVICE_EMAIL && !!SERVICE_PASSWORD;
}
// Integration tests
before(function () {
if (!hasCredentials()) {
this.skip(); // Graceful skip
}
});
🟢 GUIDELINES
Common Failure Diagnoses
When integration tests fail:
- •
Wrong API requests: Log request/response, fix mapping
- •Check debug logs with
LOG_LEVEL=debug npm run test:integration - •Compare actual vs expected request format
- •Verify endpoint URLs and HTTP methods
- •Check debug logs with
- •
Mapping issues: Compare API response vs mapped output
- •Look at debug logs showing raw API response
- •Check mapper implementation
- •Verify type conversions
- •
Auth failures: Verify credentials and method
- •Check .env file has correct credentials
- •Verify authentication flow in ConnectionTest
- •Check if API key/token is expired
- •
Type mismatches: Check generated types vs implementation
- •Verify OpenAPI spec matches reality
- •Check type generation ran successfully
- •Compare API response structure to TypeScript types
Final Validation Cycle
Run full test suite including integration tests:
npm run clean npm run build npm run test npm run test:integration # If credentials available
SUCCESS CRITERIA:
- •Zero test failures
- •Zero skipped tests (except integration without creds)
- •Build exits with code 0
Validation
Validate integration test patterns:
# Check integration tests exist ls -lh test/integration/ConnectionTest.ts ls -lh test/integration/*ProducerTest.ts # Check integration Common.ts loads dotenv grep -i "dotenv" test/integration/Common.ts && echo "✅ PASS" || echo "❌ FAIL: Should use dotenv" # Check integration tests have debug logging grep -r "getLogger" test/integration/*Test.ts | wc -l # Check hasCredentials() is used grep -r "hasCredentials()" test/integration/*Test.ts | wc -l # Check .env file exists ls -lh .env # Run integration tests (requires .env) LOG_LEVEL=debug npm run test:integration
Expected results:
- •✅ Integration tests exist for Connection and all Producers
- •✅ test/integration/Common.ts uses dotenv
- •✅ All integration tests have debug logging
- •✅ All integration tests check hasCredentials()
- •✅ .env file exists with credentials
- •✅ Integration tests pass with real API