name: integration-tests description: Guide for writing integration tests that verify CC interview behavior and runtime functionality using mock nodes. Use when user asks for help writing tests for a specific CC or feature.
Writing CC Integration Tests
This skill guides the creation of integration tests for Z-Wave Command Classes using the mock driver/controller/node infrastructure.
Reference Documents
- Test Patterns - Common assertion and setup patterns with examples
Overview
Integration tests verify CC behavior with a real driver instance talking to mock Z-Wave devices. The framework provides:
- A real
Driverinstance connected to a mock serial port - A
MockControllerthat simulates the Z-Wave controller - One or more
MockNodeinstances that simulate end devices - Auto-registered behaviors for all implemented CCs (including interview responses)
Test location: packages/zwave-js/src/lib/test/cc-specific/
Test runner: vitest (run with yarn test:ts <file>)
Reference tests:
- Simple post-interview value check:
soundSwitchInterviewValueChangeOptions.test.ts - Post-interview API command verification:
userCodeCorrectCommandByVersion.test.ts - State pre-population + unsolicited report handling:
clearUserCodesOnNotification.test.ts - Interview frame verification:
secureNodeSecureEndpoint.test.ts(intest/compliance/)
Test Lifecycle
Understanding the lifecycle is critical for writing correct tests:
- Driver starts and connects to the mock serial port
- Mock controller and node(s) are created with specified capabilities
customSetupcallback runs (if provided) — use this to add custom behaviors or pre-populate mock node state- Driver discovers the node and begins the full interview process
- Interview completes — the node emits the
"ready"event - Recorded frames are cleared (by default) — so
testBodyonly sees post-interview frames testBodyruns with access to(t, driver, node, mockController, mockNode)- Cleanup — driver is destroyed, temp cache directory removed
Key implication: By default, you cannot assert on frames sent during the interview from testBody. To inspect interview frames, set clearMessageStatsBeforeTest: false.
Single-Node Test Harness
Most tests use the single-node harness from integrationTestSuite.ts:
import { CommandClasses } from "@zwave-js/core";
import { ccCaps } from "@zwave-js/testing";
import { integrationTest } from "../integrationTestSuite.js";
integrationTest("Description of what is being tested", {
// debug: true, // Uncomment for driver logs in temp dir
nodeCapabilities: {
commandClasses: [
ccCaps({
ccId: CommandClasses["My CC"],
isSupported: true,
version: 2,
// CC-specific capabilities (type-checked via CCIdToCapabilities)
someCapability: true,
}),
],
},
testBody: async (t, driver, node, mockController, mockNode) => {
// Test assertions go here
},
});
Options
| Option | Type | Default | Description |
|---|---|---|---|
debug |
boolean |
false |
Write driver logs, keep temp dir |
provisioningDirectory |
string |
— | Copy fixture files into cache dir before start |
clearMessageStatsBeforeTest |
boolean |
true |
Clear recorded frames before testBody |
controllerCapabilities |
MockControllerOptions["capabilities"] |
— | Override controller capabilities |
nodeCapabilities |
MockNodeOptions["capabilities"] |
— | Define mock node's CCs and device info |
customSetup |
(driver, controller, node) => Promise<void> |
— | Pre-interview setup hook |
testBody |
(t, driver, node, controller, mockNode) => Promise<void> |
required | Test assertions |
additionalDriverOptions |
PartialZWaveOptions |
— | Override driver config |
Multi-Node Test Harness
For tests requiring multiple mock nodes, use integrationTestSuiteMulti.ts:
import { integrationTest } from "../integrationTestSuiteMulti.js";
integrationTest("Multi-node test", {
nodeCapabilities: [
{ id: 2, capabilities: { commandClasses: [/* ... */] } },
{ id: 3, capabilities: { commandClasses: [/* ... */] } },
],
testBody: async (t, driver, nodes, mockController, mockNodes) => {
// nodes[0] = ZWaveNode for id 2, nodes[1] = ZWaveNode for id 3
// mockNodes[0] = MockNode for id 2, etc.
},
});
Defining Node Capabilities
Basic CC list
nodeCapabilities: {
commandClasses: [
CommandClasses.Version, // Just the CC ID (defaults)
CommandClasses["Binary Switch"], // Another basic CC
],
}
CC with specific capabilities using ccCaps()
The ccCaps() helper provides type-checked CC-specific capabilities:
import { ccCaps } from "@zwave-js/testing";
nodeCapabilities: {
commandClasses: [
ccCaps({
ccId: CommandClasses["User Credential"],
isSupported: true,
version: 1,
// Fields from UserCredentialCCCapabilities:
numberOfSupportedUsers: 10,
supportedCredentialRules: [UserCredentialRule.Single],
maxUserNameLength: 32,
supportsAllUsersChecksum: true,
supportsUserChecksum: true,
supportsAdminCode: true,
supportsAdminCodeDeactivation: true,
supportedCredentialTypes: new Map([
[UserCredentialType.PINCode, {
numberOfCredentialSlots: 10,
minCredentialLength: 4,
maxCredentialLength: 10,
maxCredentialHashLength: 0,
supportsCredentialLearn: false,
}],
]),
}),
],
}
Capability types are defined in packages/testing/src/CCSpecificCapabilities.ts. Each CC maps its CommandClasses ID to a capabilities interface. Mock behaviors merge these with defaults at runtime.
Endpoints
nodeCapabilities: {
commandClasses: [/* root CCs */],
endpoints: [
{
commandClasses: [
ccCaps({ ccId: CommandClasses["Binary Switch"], isSupported: true }),
],
},
],
}
Mock Node State and Behaviors
Default Behaviors
createDefaultMockNodeBehaviors() is automatically called for every mock node. This registers handlers for all implemented CCs, including:
- Version CC queries
- All CC-specific Get → Report handlers
- Interview support (capabilities queries, user/credential enumeration, etc.)
Default behaviors are defined in packages/zwave-js/src/lib/node/mockCCBehaviors/. Each CC has its own file exporting an array of MockNodeBehavior objects.
Pre-populating Mock Node State
Mock nodes use a state map (MockNode.state) to store runtime data. CC behaviors read/write this state. To pre-populate data before the interview, use customSetup:
customSetup: async (driver, controller, mockNode) => {
// Pre-populate a user on the mock node (UserCredential CC)
mockNode.state.set("UserCredential_user_1", {
userType: UserCredentialUserType.General,
active: true,
credentialRule: UserCredentialRule.Single,
expiringTimeoutMinutes: 0,
nameEncoding: UserCredentialNameEncoding.ASCII,
userName: "Test User",
modifierType: UserCredentialModifierType.Locally,
modifierNodeId: 0,
});
// Pre-populate a credential
mockNode.state.set("UserCredential_cred_1_1_1", {
credentialData: Bytes.from("1234", "ascii"),
modifierType: UserCredentialModifierType.Locally,
modifierNodeId: 0,
});
},
State key format varies by CC — check the mock behavior file for the StateKeys object or helper functions.
Overriding Behaviors
Behaviors added via mockNode.defineBehavior() are prepended (higher priority):
customSetup: async (driver, controller, mockNode) => {
const customBehavior: MockNodeBehavior = {
handleCC(controller, self, receivedCC) {
if (receivedCC instanceof SomeCCGet) {
// Return a custom response
const cc = new SomeCCReport({
nodeId: controller.ownNodeId,
value: 42,
});
return { action: "sendCC", cc };
}
// Return undefined to fall through to default behaviors
},
};
mockNode.defineBehavior(customBehavior);
},
Behavior return values:
| Return | Effect |
|---|---|
undefined |
Fall through to next behavior |
{ action: "sendCC", cc } |
Send a CC response |
{ action: "stop" } |
Consume the frame, send nothing |
{ action: "fail" } |
Simulate a transmission failure |
{ action: "ack" } |
Send just an ACK |
Assertion Patterns
Verify Post-Interview Values
testBody: async (t, driver, node, mockController, mockNode) => {
// Check a specific value was stored
const valueId = SomeCCValues.someValue.id;
t.expect(node.getValue(valueId)).toBe(expectedValue);
// Check value metadata
const meta = node.getValueMetadata(valueId);
t.expect(meta.label).toBe("Expected Label");
// Check defined value IDs
const defined = node.getDefinedValueIDs();
const found = defined.find((v) => SomeCCValues.someValue.is(v));
t.expect(found).toBeDefined();
},
Verify Commands Sent During Interview
IMPORTANT: The receivedControllerFrames and sentControllerFrames arrays on MockNode are private. Do NOT access them directly. Always use the assertReceivedControllerFrame() / assertSentControllerFrame() methods instead.
Set clearMessageStatsBeforeTest: false to preserve interview frames:
integrationTest("Interview sends the expected commands", {
clearMessageStatsBeforeTest: false,
nodeCapabilities: {/* ... */},
testBody: async (t, driver, node, mockController, mockNode) => {
// Assert a specific command WAS sent
mockNode.assertReceivedControllerFrame(
(frame) =>
frame.type === MockZWaveFrameType.Request
&& frame.payload instanceof UserCredentialCCUserCapabilitiesGet,
{
errorMessage: "Should have sent UserCapabilitiesGet",
},
);
// Assert a specific command was NOT sent
mockNode.assertReceivedControllerFrame(
(frame) =>
frame.type === MockZWaveFrameType.Request
&& frame.payload instanceof SomeUnexpectedCommand,
{
noMatch: true,
errorMessage: "Should NOT have sent this command",
},
);
},
});
Verify Post-Interview API Commands
With default clearMessageStatsBeforeTest: true, frames from the test body are recorded:
testBody: async (t, driver, node, mockController, mockNode) => {
const api = node.commandClasses["User Credential"];
await api.someMethod(args);
mockNode.assertReceivedControllerFrame(
(frame) =>
frame.type === MockZWaveFrameType.Request
&& frame.payload instanceof ExpectedCCCommand,
{
errorMessage: "Expected command was not sent",
},
);
},
Send Unsolicited Reports to the Driver
import { createMockZWaveRequestFrame } from "@zwave-js/testing";
testBody: async (t, driver, node, mockController, mockNode) => {
const cc = new NotificationCCReport({
nodeId: mockNode.id,
notificationType: 0x06,
notificationEvent: 0x0b,
});
await mockNode.sendToController(createMockZWaveRequestFrame(cc));
// Wait for processing
await wait(100);
// Assert effect of the report
t.expect(node.getValue(someValueId)).toBe(expectedValue);
},
Pre-populate the Driver's Value Cache
testBody: async (t, driver, node, mockController, mockNode) => {
// Manually seed the value DB (simulates prior interview state)
const valueId = SomeCCValues.someValue(1).endpoint(0);
node.valueDB.setValue(valueId, "some-value");
// Verify it's stored
t.expect(node.getValue(valueId)).toBe("some-value");
},
Common Imports
// Test harness
import { integrationTest } from "../integrationTestSuite.js";
// CC-specific classes and values
import { SomeCCValues } from "@zwave-js/cc/SomeCC";
import { SomeCCGet, SomeCCReport, SomeCCSet } from "@zwave-js/cc/SomeCC";
// Enums and types from the CC (re-exported through safe entrypoint)
import { AnotherEnum, SomeEnum } from "@zwave-js/cc";
// Core types
import { CommandClasses } from "@zwave-js/core";
// Testing utilities
import {
MockZWaveFrameType,
ccCaps,
createMockZWaveRequestFrame,
} from "@zwave-js/testing";
import type { MockNodeBehavior } from "@zwave-js/testing";
// For buffer data
import { Bytes } from "@zwave-js/shared";
// For waiting
import { wait } from "alcalzone-shared/async";
Import conventions:
- CC class constructors (e.g.
UserCredentialCCUserGet): import from@zwave-js/cc/UserCredentialCC - CC Values (e.g.
UserCredentialCCValues): import from@zwave-js/cc/UserCredentialCC - Enums (e.g.
UserCredentialType,UserCredentialRule): import from@zwave-js/cc CommandClasses, core types: import from@zwave-js/coreMockZWaveFrameType,ccCaps,createMockZWaveRequestFrame,MockNodeBehavior: import from@zwave-js/testing
File Naming
Test files go in packages/zwave-js/src/lib/test/cc-specific/ and follow the pattern:
<descriptiveCamelCaseName>.test.ts
Examples:
userCredentialInterview.test.tssoundSwitchInterviewValueChangeOptions.test.tsclearUserCodesOnNotification.test.ts
Checklist for New Integration Test
Determine what to test
- Interview behavior (commands sent, values stored)
- Post-interview API behavior
- Unsolicited report handling
- Edge cases (timeouts, missing capabilities, version differences)
Set up the test
- Choose single-node or multi-node harness
- Define
nodeCapabilitieswithccCaps()for type safety - Add
customSetupif mock node state needs pre-population - Set
clearMessageStatsBeforeTest: falseif verifying interview frames
Write assertions
- Use
node.getValue()/node.getValueMetadata()for value checks - Use
mockNode.assertReceivedControllerFrame()for command checks - Use
createMockZWaveRequestFrame()+mockNode.sendToController()for unsolicited reports
- Use
Run and validate
-
yarn test:ts <test-file-path> -
yarn fmt
-