ワンクリックで
integration-tests
// 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.
// 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.
| 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. |
This skill guides the creation of integration tests for Z-Wave Command Classes using the mock driver/controller/node infrastructure.
Integration tests verify CC behavior with a real driver instance talking to mock Z-Wave devices. The framework provides:
Driver instance connected to a mock serial portMockController that simulates the Z-Wave controllerMockNode instances that simulate end devicesTest location: packages/zwave-js/src/lib/test/cc-specific/
Test runner: vitest (run with yarn test:ts <file>)
Reference tests:
soundSwitchInterviewValueChangeOptions.test.tsuserCodeCorrectCommandByVersion.test.tsclearUserCodesOnNotification.test.tssecureNodeSecureEndpoint.test.ts (in test/compliance/)Understanding the lifecycle is critical for writing correct tests:
customSetup callback runs (if provided) — use this to add custom behaviors or pre-populate mock node state"ready" eventtestBody only sees post-interview framestestBody runs with access to (t, driver, node, mockController, mockNode)Key implication: By default, you cannot assert on frames sent during the interview from testBody. To inspect interview frames, set clearMessageStatsBeforeTest: false.
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
},
});
| 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 |
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.
},
});
nodeCapabilities: {
commandClasses: [
CommandClasses.Version, // Just the CC ID (defaults)
CommandClasses["Binary Switch"], // Another basic CC
],
}
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.
nodeCapabilities: {
commandClasses: [/* root CCs */],
endpoints: [
{
commandClasses: [
ccCaps({ ccId: CommandClasses["Binary Switch"], isSupported: true }),
],
},
],
}
createDefaultMockNodeBehaviors() is automatically called for every mock node. This registers handlers for all implemented CCs, including:
Default behaviors are defined in packages/zwave-js/src/lib/node/mockCCBehaviors/. Each CC has its own file exporting an array of MockNodeBehavior objects.
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.
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 |
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();
},
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",
},
);
},
});
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",
},
);
},
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);
},
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");
},
// 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:
UserCredentialCCUserGet): import from @zwave-js/cc/UserCredentialCCUserCredentialCCValues): import from @zwave-js/cc/UserCredentialCCUserCredentialType, UserCredentialRule): import from @zwave-js/ccCommandClasses, core types: import from @zwave-js/coreMockZWaveFrameType, ccCaps, createMockZWaveRequestFrame, MockNodeBehavior: import from @zwave-js/testingTest 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.tsDetermine what to test
Set up the test
nodeCapabilities with ccCaps() for type safetycustomSetup if mock node state needs pre-populationclearMessageStatsBeforeTest: false if verifying interview framesWrite assertions
node.getValue() / node.getValueMetadata() for value checksmockNode.assertReceivedControllerFrame() for command checkscreateMockZWaveRequestFrame() + mockNode.sendToController() for unsolicited reportsRun and validate
yarn test:ts <test-file-path>yarn fmt