Factor #6: Portability - Tests Run the Same Everywhere

"It works on my machine" is not acceptable for tests. A test should produce identical results whether it's running on your laptop, your colleague's Docker container, or GitHub Actions.

Common Portability Killers

Hardcoded Paths
// 😱 BAD: Absolute paths that only exist on one machine
test('loads config file', () => {
  const config = loadConfig('/Users/alice/project/config.json');
  expect(config.apiUrl).toBe('https://api.example.com');
});

test('saves to temp directory', () => {
  const file = saveTemp('/tmp/test-output.txt', data);
  // Fails on Windows: no /tmp directory!
  expect(fs.existsSync('/tmp/test-output.txt')).toBe(true);
});

// ✅ GOOD: Use relative paths and cross-platform abstractions
test('loads config file', () => {
  const config = loadConfig(path.join(__dirname, 'fixtures', 'config.json'));
  expect(config.apiUrl).toBe('https://api.example.com');
});

test('saves to temp directory', () => {
  const tempDir = os.tmpdir(); // Works on all platforms
  const tempFile = path.join(tempDir, 'test-output.txt');
  saveTemp(tempFile, data);
  expect(fs.existsSync(tempFile)).toBe(true);
});
Environment-Specific Dependencies
// 😱 BAD: Assumes specific tools or services are running
test('processes image', () => {
  // Assumes ImageMagick is installed
  exec('convert input.jpg -resize 100x100 output.jpg');
  
  // Assumes Redis is running on default port
  const redis = new Redis({ host: 'localhost', port: 6379 });
  redis.set('key', 'value');
});

test('sends email', () => {
  // Assumes SMTP server on localhost
  sendEmail({
    host: 'localhost',
    port: 25,
    to: 'test@example.com'
  });
});

// ✅ GOOD: Mock or containerize external dependencies
test('processes image', () => {
  // Use a JS library instead of system dependency
  const sharp = require('sharp');
  await sharp('input.jpg').resize(100, 100).toFile('output.jpg');
});

test('caching works', () => {
  // Use in-memory mock or test container
  const cache = new Map(); // or use testcontainers for real Redis
  cache.set('key', 'value');
  expect(cache.get('key')).toBe('value');
});
Timing and Timezone Dependencies
// 😱 BAD: Tests fail in different timezones or at different times
test('formats date correctly', () => {
  const date = new Date('2024-01-15 10:00:00');
  expect(formatDate(date)).toBe('Jan 15, 10:00 AM');
  // Fails in different timezone!
});

test('business hours check', () => {
  const now = new Date();
  // Fails when run at night or weekends!
  expect(isBusinessHours(now)).toBe(true);
});

// ✅ GOOD: Control time and timezone in tests
test('formats date correctly', () => {
  // Use fixed timezone
  const date = new Date('2024-01-15T10:00:00Z');
  expect(formatDate(date, 'UTC')).toBe('Jan 15, 10:00 AM');
});

test('business hours check', () => {
  // Mock the current time
  const mockDate = new Date('2024-01-15T14:00:00Z'); // Monday 2 PM
  jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime());
  
  expect(isBusinessHours()).toBe(true);
  
  const weekend = new Date('2024-01-14T14:00:00Z'); // Sunday
  Date.now.mockReturnValue(weekend.getTime());
  expect(isBusinessHours()).toBe(false);
});
Network and External API Dependencies
// 😱 BAD: Tests fail when offline or API is down
test('fetches user data', async () => {
  const user = await fetch('https://api.github.com/users/octocat');
  expect(user.login).toBe('octocat');
  // Fails when: offline, API down, rate limited, response changes
});

test('geocoding works', async () => {
  const coords = await geocode('New York');
  expect(coords.lat).toBeCloseTo(40.7128);
  // Fails without internet or API key
});

// ✅ GOOD: Mock external APIs
test('fetches user data', async () => {
  // Mock the API call
  fetchMock.get('https://api.github.com/users/octocat', {
    login: 'octocat',
    id: 583231,
    name: 'The Octocat'
  });
  
  const user = await fetchGitHubUser('octocat');
  expect(user.login).toBe('octocat');
});

// Or use recorded fixtures (like VCR)
test('geocoding works', async () => {
  // Use recorded response
  const coords = await geocodeWithFixture('New York');
  expect(coords.lat).toBeCloseTo(40.7128);
});

Achieving True Portability

1. Containerize Test Dependencies
Use Docker or testcontainers for external services
// Use testcontainers for real services
import { GenericContainer } from 'testcontainers';

beforeAll(async () => {
  const redis = await new GenericContainer('redis')
    .withExposedPorts(6379)
    .start();
  
  process.env.REDIS_URL = `redis://${redis.getHost()}:${redis.getMappedPort(6379)}`;
});
2. Use Environment Variables
Never hardcode configuration
// .env.test
DATABASE_URL=sqlite::memory:
API_KEY=test-key-123
TEMP_DIR=./test-temp

// test setup
require('dotenv').config({ path: '.env.test' });

// Now tests work everywhere with same config
3. Abstract Platform Differences
Hide OS-specific code behind interfaces
// Platform abstraction
class FileSystem {
  static getTempDir() {
    return process.platform === 'win32' 
      ? process.env.TEMP 
      : '/tmp';
  }
  
  static getPathSeparator() {
    return path.sep; // Handles / vs \
  }
  
  static normalize(filepath) {
    return path.normalize(filepath);
  }
}
4. Document Requirements Clearly
Make dependencies explicit
// package.json
{
  "scripts": {
    "test": "jest",
    "test:ci": "jest --ci --coverage",
    "test:setup": "docker-compose up -d test-deps"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}

// README.md
## Running Tests
```bash
# Install dependencies
npm install

# Start test containers (PostgreSQL, Redis)
npm run test:setup

# Run tests
npm test
```

The Portability Checklist

Testing in Multiple Environments

CI Matrix Testing
# GitHub Actions example
strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    node: [18, 20, 22]
    
runs-on: ${{ matrix.os }}
steps:
  - uses: actions/setup-node@v4
    with:
      node-version: ${{ matrix.node }}
  - run: npm test