@pmouli/hap-test
v0.4.0
Published
Test harness for Homebridge plugin development - write integration tests without physical HomeKit controllers
Maintainers
Readme
hap-test
Comprehensive test harness for Homebridge plugin development
Overview
hap-test is a comprehensive testing framework for Homebridge plugins that enables developers to write integration and end-to-end tests without requiring physical HomeKit controllers or a running Homebridge instance. Test your plugins with confidence using mocked Homebridge APIs, simulated HomeKit interactions, and deterministic time control.
Why hap-test?
- 🚀 Fast: No network access, no physical devices, tests complete in milliseconds
- 🎯 Deterministic: Fake timers eliminate flaky tests from timing issues
- ✅ Complete: Full Homebridge API mock with lifecycle events
- 🛡️ Type-Safe: Full TypeScript support with comprehensive types
- 📊 Validated: Automatic HAP protocol validation catches bugs early
- 🧪 Flexible: Test success paths, error scenarios, and edge cases
- 🔍 Debuggable: Debug mode with detailed logging of all operations
Features
Core Testing Infrastructure
- TestHarness: Orchestrates complete Homebridge environment for testing
- MockHomebridgeAPI: Complete mock of Homebridge platform API
- MockHomeKit: Simulates Apple Home controller interactions
- TimeController: Deterministic time-based testing with Vitest integration
Advanced Capabilities
- HAP Protocol Validation: Automatic validation of characteristic constraints (min/max/step/format)
- Event System: Subscribe to and test characteristic value change events
- Network Simulation: Test resilience with simulated latency, packet loss, and disconnection
- Cached Accessories: Test accessory restoration across Homebridge restarts
- Multi-User Support: Test scenarios with multiple HomeKit controllers
- Custom Matchers: Vitest matchers for common HomeKit assertions
Installation
npm install --save-dev hap-test
# or
pnpm add -D hap-test
# or
yarn add -D hap-testPeer Dependencies
npm install --save-dev homebridge hap-nodejs hap-fluent vitestRequired Versions:
homebridge>= 1.11.0hap-nodejs>= 0.13.0hap-fluent>= 0.3.0vitest>= 2.0.0
Quick Start
Here's a complete test for a lightbulb platform:
import { describe, test, expect, afterEach } from 'vitest';
import { TestHarness } from 'hap-test';
import { LightbulbPlatform } from '../src/platform';
describe('LightbulbPlatform', () => {
let harness: TestHarness;
afterEach(async () => {
if (harness) {
await harness.shutdown();
}
});
test('should register lightbulb accessory', async () => {
// Create test harness with your platform
harness = await TestHarness.create({
platformConstructor: LightbulbPlatform,
platformConfig: {
platform: 'LightbulbPlatform',
name: 'Test Lightbulb',
devices: [{ id: 'light-1', name: 'Living Room' }],
},
});
// Wait for platform to register accessories
await harness.waitForAccessories(1);
// Get registered accessory
const accessories = harness.homekit.accessories();
expect(accessories).toHaveLength(1);
expect(accessories[0].displayName).toBe('Living Room');
// Get lightbulb service
const service = harness.homekit.service(accessories[0].UUID, 'Lightbulb');
expect(service).toBeDefined();
// Test characteristic operations
const onChar = service?.getCharacteristic('On');
expect(onChar?.value).toBe(false);
// Simulate HomeKit setting value
await service?.setCharacteristic('On', true);
expect(onChar?.value).toBe(true);
});
});Core Concepts
TestHarness
The central orchestrator that manages the complete test environment:
const harness = await TestHarness.create({
// Your platform class
platformConstructor: MyPlatform,
// Platform configuration
platformConfig: {
platform: 'MyPlatform',
name: 'Test Platform',
// ... your config
},
// Optional: cached accessories from previous session
cachedAccessories: [],
// Optional: enable debug logging
debug: true,
// Optional: custom timeouts
timeouts: {
startup: 5000,
registration: 3000,
},
});
// Access components
harness.homekit; // MockHomeKit controller
harness.api; // MockHomebridgeAPI
harness.time; // TimeController
harness.network; // NetworkSimulator
// Cleanup
await harness.shutdown();MockHomeKit
Simulates an Apple Home controller:
// Get all registered accessories
const accessories = harness.homekit.accessories();
// Get specific accessory
const accessory = harness.homekit.accessory('accessory-uuid');
// Get service on accessory
const service = harness.homekit.service('accessory-uuid', 'Lightbulb');
// Get characteristic
const char = harness.homekit.characteristic('accessory-uuid', 'Lightbulb', 'On');
// Simulate HomeKit operations
await service.setCharacteristic('On', true);
const value = await service.getCharacteristic('On');
// Subscribe to events
const subscription = char.subscribe();
const event = await subscription.waitForNext(1000);TimeController
Control time for deterministic testing:
// Advance time by 30 seconds
await harness.time.advance(30000);
// Freeze time at specific point
harness.time.freeze();
// Set specific time
harness.time.setTime(new Date('2025-01-01T00:00:00Z'));
// Get current fake time
const now = harness.time.now();
// Reset to real time
harness.time.reset();NetworkSimulator
Simulate network conditions:
// Enable network simulation
const harness = await TestHarness.create({
platformConfig: myConfig,
networkSimulation: { enabled: true },
});
// Add latency (affects all operations)
harness.network.setLatency(200); // 200ms delay
// Simulate packet loss
harness.network.setPacketLoss(0.1); // 10% packet loss
// Disconnect network
harness.network.disconnect();
// Reconnect
harness.network.reconnect();
// Reset to normal
harness.network.reset();Advanced Features
Event Subscriptions
Test asynchronous platform behavior:
const char = harness.homekit.characteristic(uuid, 'Lightbulb', 'On');
// Subscribe to value changes
const subscription = char.subscribe();
// Platform changes value
await platform.updateCharacteristic();
// Wait for event
const event = await subscription.waitForNext(5000);
expect(event.newValue).toBe(true);
// Check event history
const history = subscription.getHistory();
console.log('Events:', history);
// Cleanup
subscription.unsubscribe();HAP Protocol Validation
Automatic validation of HAP constraints:
const service = harness.homekit.service(uuid, 'Lightbulb');
// ✅ Valid operation
await service.setCharacteristic('Brightness', 75);
// ❌ Throws CharacteristicValidationError
await expect(
service.setCharacteristic('Brightness', 150) // Max is 100
).rejects.toThrow('exceeds maximum 100');
// ❌ Throws for wrong format
await expect(
service.setCharacteristic('Brightness', 'bright') // Must be number
).rejects.toThrow('Expected number');
// ❌ Throws for invalid enum
await expect(
service.setCharacteristic('TargetHeatingCoolingState', 99)
).rejects.toThrow('not in valid values');Cached Accessories
Test accessory restoration across restarts:
// First session - register accessories
let harness = await TestHarness.create({
platformConstructor: MyPlatform,
platformConfig: myConfig,
});
await harness.waitForAccessories(2);
const cached = harness.api.getRegisteredAccessories();
await harness.shutdown();
// Second session - restore from cache
harness = await TestHarness.create({
platformConstructor: MyPlatform,
platformConfig: myConfig,
cachedAccessories: cached, // Restored!
});
// Platform's configureAccessory() was called
expect(harness.homekit.accessories()).toHaveLength(2);Time-Based Testing
Test polling, schedules, and timeouts:
test('polls device every 30 seconds', async () => {
const harness = await TestHarness.create({
platformConstructor: PollingPlatform,
platformConfig: myConfig,
});
const char = harness.homekit.characteristic(uuid, 'Lightbulb', 'On');
// Initial state
expect(char.value).toBe(false);
// Advance 30 seconds
await harness.time.advance(30000);
// Platform should have polled
expect(char.value).toBe(true);
// Advance another 30 seconds
await harness.time.advance(30000);
// Another poll occurred
expect(char.value).toBe(false);
});Error Scenarios
Test error handling and recovery:
test('handles device errors gracefully', async () => {
const harness = await TestHarness.create({
platformConstructor: MyPlatform,
platformConfig: myConfig,
networkSimulation: { enabled: true },
});
const service = harness.homekit.service(uuid, 'Lightbulb');
// Simulate network failure
harness.network.disconnect();
// Operation should fail
await expect(
service.setCharacteristic('On', true)
).rejects.toThrow('Network disconnected');
// Reconnect
harness.network.reconnect();
// Should work now
await service.setCharacteristic('On', true);
expect(service.getCharacteristic('On')?.value).toBe(true);
});Custom Matchers
Vitest matchers for common assertions:
import 'hap-test/matchers';
test('using custom matchers', async () => {
const harness = await TestHarness.create({...});
const accessory = harness.homekit.accessory(uuid);
const service = harness.homekit.service(uuid, 'Lightbulb');
const char = harness.homekit.characteristic(uuid, 'Lightbulb', 'Brightness');
// Accessory matchers
expect(accessory).toBeRegistered();
expect(accessory).toHaveService('Lightbulb');
// Service matchers
expect(service).toHaveCharacteristic('On');
expect(service).toHaveCharacteristic('Brightness');
// Characteristic matchers
expect(char).toHaveValue(75);
expect(char).toBeInRange(0, 100);
expect(char).toHaveFormat('int');
});Examples
Check the /examples directory for complete working examples:
- basic-accessory-test.ts - Simple lightbulb platform test
- debug-mode.ts - Using debug logging
- error-scenarios.ts - Testing error handling
- hap-protocol-validation.ts - HAP validation examples
- multi-device-platform.ts - Multiple accessories
- time-based-features.ts - Polling and schedules
Documentation
- Getting Started - Your first test in 10 minutes
- Advanced Testing - Events, time control, network simulation
- Migration Guide - From manual to automated testing
- Troubleshooting - Common issues and solutions
- API Reference - Complete API documentation
Testing Best Practices
1. Always Clean Up
let harness: TestHarness;
afterEach(async () => {
if (harness) {
await harness.shutdown();
}
});2. Wait for Registration
const harness = await TestHarness.create({...});
await harness.waitForAccessories(expectedCount, timeout);3. Use Time Control
// ❌ Flaky - uses real time
await new Promise(resolve => setTimeout(resolve, 1000));
// ✅ Deterministic - uses fake time
await harness.time.advance(1000);4. Enable Debug Mode
const harness = await TestHarness.create({
platformConfig: myConfig,
debug: true, // See all operations
});5. Test Error Cases
// Test both success and failure
test('handles invalid values', async () => {
await expect(
service.setCharacteristic('Brightness', -1)
).rejects.toThrow();
});TypeScript Configuration
Add to your tsconfig.json:
{
"compilerOptions": {
"strict": true,
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"types": ["vitest/globals", "node"]
},
"include": ["test/**/*.ts"]
}Vitest Configuration
Add to your vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
enabled: true,
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/**/*.ts'],
},
},
});Performance
hap-test is designed for speed:
- Harness initialization: < 50ms
- Accessory registration: < 10ms per accessory
- Characteristic operations: < 1ms
- Full test suite: Typically < 1 second
Compatibility
- Homebridge: 1.11.0+
- HAP-NodeJS: 0.13.0+
- Node.js: 18+
- TypeScript: 5.0+
- Vitest: 2.0+
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
License
MIT © Pradeep Mouli
Support
Acknowledgments
- Built for Homebridge plugin development
- Works seamlessly with hap-fluent for type-safe characteristic access
- Powered by Vitest for fast, modern testing
Status: Production Ready | Version: 0.1.0 | Tests: 134 passing
format: 'bool',
perms: ['pr', 'pw', 'ev'],
});
service.addCharacteristic(onChar);
accessory.addService(service);
harness.homeKit.addAccessory(accessory);
// Test characteristic operations
await onChar.setValue(true);
expect(await onChar.getValue()).toBe(true);
// Test event subscriptions
const subscription = onChar.subscribe();
await onChar.setValue(false);
const history = subscription.getHistory();
expect(history.length).toBeGreaterThan(0);
expect(history[history.length - 1].newValue).toBe(false);
subscription.unsubscribe();
harness.shutdown();}); });
See [examples/basic-accessory-test.ts](./examples/basic-accessory-test.ts) for more detailed usage.
## Documentation
- [Getting Started](./docs/getting-started.md)
- [Advanced Testing](./docs/advanced-testing.md)
- [Migration Guide](./docs/migration-guide.md)
- [API Reference (summary)](./docs/api.md) — generate HTML via `pnpm --filter hap-test docs:api`
- [Examples](./examples/)
## Requirements
- Node.js >= 18.0.0
- Homebridge >= 1.6.0 or 2.x
- HAP-NodeJS >= 0.11.0
- Vitest >= 1.0.0
## Development Status
🚧 **Phase 6 (Developer Experience) - In Progress**
Current capabilities:
- ✅ Core infrastructure (TestHarness, MockHomebridgeAPI, MockHomeKit, TimeController)
- ✅ HAP protocol validation and permissions enforcement
- ✅ Event subscriptions and history tracking
- ✅ Network simulation (latency, packet loss, disconnect)
- ✅ Custom Vitest matchers
- ✅ Example suites covering common devices
API may change before 1.0.0 release.
## License
MIT © HAP Fluent Monorepo Contributors