@iobroker/testing 2.6.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@
4
4
  PLACEHOLDER for next version:
5
5
  ## __WORK IN PROGRESS__
6
6
  -->
7
+ ## 3.0.0 (2022-05-09)
8
+ * BREAKING: The function signature of `defineAdditionalTests` in integration tests has changed. All user-defined integration tests must now be grouped in one or more `suite` blocks. The adapter will now only be started at the beginning of each suite. See the documentation for details.
9
+ * BREAKING: The function signature of `harness.startAdapterAndWait` has changed. It now accepts a boolean as the first parameter which controls whether to wait for the `alive` state (`false`) or the `info.connection` state (`true`).
10
+
7
11
  ## 2.6.0 (2022-04-18)
8
12
  * The loglevel for the adapter and DB instances is now configurable and defaults to `"debug"` in both cases
9
13
 
package/README.md CHANGED
@@ -26,29 +26,6 @@ tests.packageFiles(path.join(__dirname, ".."));
26
26
  // This should be the adapter's root directory
27
27
  ```
28
28
 
29
- ### Adapter startup (Unit test)
30
-
31
- **Unit tests for adapter startup were removed and are essentially a no-op now.**
32
- If you defined your own tests, they should still work.
33
-
34
- ```ts
35
- const path = require("path");
36
- const { tests } = require("@iobroker/testing");
37
-
38
- tests.unit(path.join(__dirname, ".."), {
39
- // ~~~~~~~~~~~~~~~~~~~~~~~~~
40
- // This should be the adapter's root directory
41
-
42
- // Define your own tests inside defineAdditionalTests.
43
- // If you need predefined objects etc. here, you need to take care of it yourself
44
- defineAdditionalTests() {
45
- it("works", () => {
46
- // see below how these could look like
47
- });
48
- },
49
- });
50
- ```
51
-
52
29
  ### Adapter startup (Integration test)
53
30
 
54
31
  Run the following snippet in a `mocha` test file to test the adapter startup process against a real JS-Controller instance:
@@ -67,13 +44,15 @@ tests.integration(path.join(__dirname, ".."), {
67
44
  allowedExitCodes: [11],
68
45
 
69
46
  // Define your own tests inside defineAdditionalTests
70
- // Since the tests are heavily instrumented, you need to create and use a so called "harness" to control the tests.
71
- defineAdditionalTests(getHarness) {
72
- describe("Test sendTo()", () => {
47
+ defineAdditionalTests({ suite }) {
48
+ // All tests (it, describe) must be grouped in one or more suites. Each suite sets up a fresh environment for the adapter tests.
49
+ // At the beginning of each suite, the databases will be reset and the adapter will be started.
50
+ // The adapter will run until the end of each suite.
51
+
52
+ // Since the tests are heavily instrumented, each suite gives access to a so called "harness" to control the tests.
53
+ suite("Test sendTo()", (harness) => {
73
54
  it("Should work", () => {
74
55
  return new Promise(async (resolve) => {
75
- // Create a fresh harness instance each test!
76
- const harness = getHarness();
77
56
  // Start the adapter and wait until it has started
78
57
  await harness.startAdapterAndWait();
79
58
 
@@ -89,6 +68,29 @@ tests.integration(path.join(__dirname, ".."), {
89
68
  });
90
69
  ```
91
70
 
71
+ ### Adapter startup (Unit test)
72
+
73
+ **Unit tests for adapter startup were removed and are essentially a no-op now.**
74
+ If you defined your own tests, they should still work.
75
+
76
+ ```ts
77
+ const path = require("path");
78
+ const { tests } = require("@iobroker/testing");
79
+
80
+ tests.unit(path.join(__dirname, ".."), {
81
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~
82
+ // This should be the adapter's root directory
83
+
84
+ // Define your own tests inside defineAdditionalTests.
85
+ // If you need predefined objects etc. here, you need to take care of it yourself
86
+ defineAdditionalTests() {
87
+ it("works", () => {
88
+ // see below how these could look like
89
+ });
90
+ },
91
+ });
92
+ ```
93
+
92
94
  ### Helper functions for your own tests
93
95
 
94
96
  Under `utils`, several functions are exposed to use in your own tests:
@@ -1,4 +1,5 @@
1
1
  /// <reference types="iobroker" />
2
+ /// <reference types="mocha" />
2
3
  import { TestHarness } from "./lib/harness";
3
4
  export interface TestAdapterOptions {
4
5
  allowedExitCodes?: (number | string)[];
@@ -7,6 +8,17 @@ export interface TestAdapterOptions {
7
8
  /** How long to wait before the adapter startup is considered successful */
8
9
  waitBeforeStartupSuccess?: number;
9
10
  /** Allows you to define additional tests */
10
- defineAdditionalTests?: (getHarness: () => TestHarness) => void;
11
+ defineAdditionalTests?: (args: TestContext) => void;
12
+ }
13
+ export interface TestContext {
14
+ /**
15
+ * Defines a test suite. At the start of each suite, the adapter will be started with a fresh environment.
16
+ * To define tests in each suite, use describe and it as usual.
17
+ *
18
+ * Each suite has its own test harness, which gets passed as an argument.
19
+ */
20
+ suite: (name: string, fn: (harness: TestHarness) => void) => void;
21
+ describe: Mocha.SuiteFunction;
22
+ it: Mocha.TestFunction;
11
23
  }
12
24
  export declare function testAdapter(adapterDir: string, options?: TestAdapterOptions): void;
@@ -41,113 +41,157 @@ function testAdapter(adapterDir, options = {}) {
41
41
  let dbConnection;
42
42
  let harness;
43
43
  const controllerSetup = new controllerSetup_1.ControllerSetup(adapterDir, testDir);
44
+ let objectsBackup;
45
+ let statesBackup;
46
+ let isInSuite = false;
44
47
  console.log();
45
48
  console.log(`Running tests in ${testDir}`);
46
49
  console.log();
47
- describe(`Test the adapter (in a live environment)`, () => {
48
- let objectsBackup;
49
- let statesBackup;
50
- before(async function () {
51
- var _a;
52
- // Installation may take a while - especially if rsa-compat needs to be installed
53
- const oneMinute = 60000;
54
- this.timeout(30 * oneMinute);
55
- if (await controllerSetup.isJsControllerRunning()) {
56
- throw new Error("JS-Controller is already running! Stop it for the first test run and try again!");
57
- }
58
- const adapterSetup = new adapterSetup_1.AdapterSetup(adapterDir, testDir);
59
- // Installation happens in two steps:
60
- // First we need to set up JS Controller, so the databases etc. can be created
61
- // First we need to copy all files and execute an npm install
62
- await controllerSetup.prepareTestDir();
63
- // Only then we can install the adapter, because some (including VIS) try to access
64
- // the databases if JS Controller is installed
65
- await adapterSetup.installAdapterInTestDir();
66
- const dbConnection = new dbConnection_1.DBConnection(appName, testDir, (0, logger_1.createLogger)((_a = options.loglevel) !== null && _a !== void 0 ? _a : "debug"));
67
- await dbConnection.start();
68
- controllerSetup.setupSystemConfig(dbConnection);
69
- await controllerSetup.disableAdminInstances(dbConnection);
70
- await adapterSetup.deleteOldInstances(dbConnection);
71
- await adapterSetup.addAdapterInstance();
72
- await dbConnection.stop();
73
- // Create a copy of the databases that we can restore later
74
- ({ objects: objectsBackup, states: statesBackup } =
75
- await dbConnection.backup());
50
+ async function prepareTests() {
51
+ var _a;
52
+ // Installation may take a while - especially if rsa-compat needs to be installed
53
+ const oneMinute = 60000;
54
+ this.timeout(30 * oneMinute);
55
+ if (await controllerSetup.isJsControllerRunning()) {
56
+ throw new Error("JS-Controller is already running! Stop it for the first test run and try again!");
57
+ }
58
+ const adapterSetup = new adapterSetup_1.AdapterSetup(adapterDir, testDir);
59
+ // Installation happens in two steps:
60
+ // First we need to set up JS Controller, so the databases etc. can be created
61
+ // First we need to copy all files and execute an npm install
62
+ await controllerSetup.prepareTestDir();
63
+ // Only then we can install the adapter, because some (including VIS) try to access
64
+ // the databases if JS Controller is installed
65
+ await adapterSetup.installAdapterInTestDir();
66
+ const dbConnection = new dbConnection_1.DBConnection(appName, testDir, (0, logger_1.createLogger)((_a = options.loglevel) !== null && _a !== void 0 ? _a : "debug"));
67
+ await dbConnection.start();
68
+ controllerSetup.setupSystemConfig(dbConnection);
69
+ await controllerSetup.disableAdminInstances(dbConnection);
70
+ await adapterSetup.deleteOldInstances(dbConnection);
71
+ await adapterSetup.addAdapterInstance();
72
+ await dbConnection.stop();
73
+ // Create a copy of the databases that we can restore later
74
+ ({ objects: objectsBackup, states: statesBackup } =
75
+ await dbConnection.backup());
76
+ }
77
+ async function shutdownTests() {
78
+ // Stopping the processes may take a while
79
+ this.timeout(30000);
80
+ // Stop the controller again
81
+ await harness.stopController();
82
+ harness.removeAllListeners();
83
+ }
84
+ async function resetDbAndStartHarness() {
85
+ var _a, _b;
86
+ this.timeout(30000);
87
+ dbConnection = new dbConnection_1.DBConnection(appName, testDir, (0, logger_1.createLogger)((_a = options.loglevel) !== null && _a !== void 0 ? _a : "debug"));
88
+ // Clean up before every single test
89
+ await Promise.all([
90
+ controllerSetup.clearDBDir(),
91
+ controllerSetup.clearLogDir(),
92
+ dbConnection.restore(objectsBackup, statesBackup),
93
+ ]);
94
+ // Create a new test harness
95
+ await dbConnection.start();
96
+ harness = new harness_1.TestHarness(adapterDir, testDir, dbConnection);
97
+ // Enable the adapter and set its loglevel to the selected one
98
+ await harness.changeAdapterConfig(adapterName, {
99
+ common: {
100
+ enabled: true,
101
+ loglevel: (_b = options.loglevel) !== null && _b !== void 0 ? _b : "debug",
102
+ },
76
103
  });
77
- beforeEach(async function () {
78
- var _a, _b;
79
- this.timeout(30000);
80
- dbConnection = new dbConnection_1.DBConnection(appName, testDir, (0, logger_1.createLogger)((_a = options.loglevel) !== null && _a !== void 0 ? _a : "debug"));
81
- // Clean up before every single test
82
- await Promise.all([
83
- controllerSetup.clearDBDir(),
84
- controllerSetup.clearLogDir(),
85
- dbConnection.restore(objectsBackup, statesBackup),
86
- ]);
87
- // Create a new test harness
88
- await dbConnection.start();
89
- harness = new harness_1.TestHarness(adapterDir, testDir, dbConnection);
90
- // Enable the adapter and set its loglevel to the selected one
91
- await harness.changeAdapterConfig(adapterName, {
92
- common: {
93
- enabled: true,
94
- loglevel: (_b = options.loglevel) !== null && _b !== void 0 ? _b : "debug",
95
- },
104
+ // And enable the sendTo emulation
105
+ await harness.enableSendTo();
106
+ }
107
+ describe(`Adapter integration tests`, () => {
108
+ before(prepareTests);
109
+ describe("Adapter startup", () => {
110
+ beforeEach(resetDbAndStartHarness);
111
+ afterEach(shutdownTests);
112
+ it("The adapter starts", function () {
113
+ var _a;
114
+ this.timeout(60000);
115
+ const allowedExitCodes = new Set((_a = options.allowedExitCodes) !== null && _a !== void 0 ? _a : []);
116
+ // Adapters with these modes are allowed to "immediately" exit with code 0
117
+ switch (harness.getAdapterExecutionMode()) {
118
+ case "schedule":
119
+ case "once":
120
+ case "subscribe":
121
+ allowedExitCodes.add(0);
122
+ }
123
+ return new Promise((resolve, reject) => {
124
+ // Register a handler to check the alive state and exit codes
125
+ harness
126
+ .on("stateChange", async (id, state) => {
127
+ if (id ===
128
+ `system.adapter.${adapterName}.0.alive` &&
129
+ state &&
130
+ state.val === true) {
131
+ // Wait a bit so we can catch errors that do not happen immediately
132
+ await (0, async_1.wait)(options.waitBeforeStartupSuccess !=
133
+ undefined
134
+ ? options.waitBeforeStartupSuccess
135
+ : 5000);
136
+ resolve(`The adapter started successfully.`);
137
+ }
138
+ })
139
+ .on("failed", (code) => {
140
+ if (!allowedExitCodes.has(code)) {
141
+ reject(new Error(`The adapter startup was interrupted unexpectedly with ${typeof code === "number"
142
+ ? "code"
143
+ : "signal"} ${code}`));
144
+ }
145
+ else {
146
+ // This was a valid exit code
147
+ resolve(`The expected ${typeof code === "number"
148
+ ? "exit code"
149
+ : "signal"} ${code} was received.`);
150
+ }
151
+ });
152
+ harness.startAdapter();
153
+ }).then((msg) => console.log(msg));
96
154
  });
97
- // And enable the sendTo emulation
98
- await harness.enableSendTo();
99
- });
100
- afterEach(async function () {
101
- // Stopping the processes may take a while
102
- this.timeout(30000);
103
- // Stop the controller again
104
- await harness.stopController();
105
- harness.removeAllListeners();
106
- });
107
- it("The adapter starts", function () {
108
- var _a;
109
- this.timeout(60000);
110
- const allowedExitCodes = new Set((_a = options.allowedExitCodes) !== null && _a !== void 0 ? _a : []);
111
- // Adapters with these modes are allowed to "immediately" exit with code 0
112
- switch (harness.getAdapterExecutionMode()) {
113
- case "schedule":
114
- case "once":
115
- case "subscribe":
116
- allowedExitCodes.add(0);
117
- }
118
- return new Promise((resolve, reject) => {
119
- // Register a handler to check the alive state and exit codes
120
- harness
121
- .on("stateChange", async (id, state) => {
122
- if (id === `system.adapter.${adapterName}.0.alive` &&
123
- state &&
124
- state.val === true) {
125
- // Wait a bit so we can catch errors that do not happen immediately
126
- await (0, async_1.wait)(options.waitBeforeStartupSuccess != undefined
127
- ? options.waitBeforeStartupSuccess
128
- : 5000);
129
- resolve(`The adapter started successfully.`);
130
- }
131
- })
132
- .on("failed", (code) => {
133
- if (!allowedExitCodes.has(code)) {
134
- reject(new Error(`The adapter startup was interrupted unexpectedly with ${typeof code === "number"
135
- ? "code"
136
- : "signal"} ${code}`));
137
- }
138
- else {
139
- // This was a valid exit code
140
- resolve(`The expected ${typeof code === "number"
141
- ? "exit code"
142
- : "signal"} ${code} was received.`);
143
- }
144
- });
145
- harness.startAdapter();
146
- }).then((msg) => console.log(msg));
147
155
  });
148
156
  // Call the user's tests
149
157
  if (typeof options.defineAdditionalTests === "function") {
150
- options.defineAdditionalTests(() => harness);
158
+ const originalIt = global.it;
159
+ // Ensure no it() gets called outside of a suite()
160
+ function assertSuite() {
161
+ if (!isInSuite) {
162
+ throw new Error("In user-defined adapter tests, it() must NOT be called outside of a suite()");
163
+ }
164
+ }
165
+ const patchedIt = new Proxy(originalIt, {
166
+ apply(target, thisArg, args) {
167
+ assertSuite();
168
+ return target.apply(thisArg, args);
169
+ },
170
+ get(target, propKey) {
171
+ assertSuite();
172
+ return target[propKey];
173
+ },
174
+ });
175
+ describe("User-defined tests", () => {
176
+ // patch the global it() function so nobody can bypass the checks
177
+ global.it = patchedIt;
178
+ const args = {
179
+ // a test suite is a special describe which sets up and tears down the test environment before and after ALL tests
180
+ suite: (name, fn) => {
181
+ describe(name, () => {
182
+ isInSuite = true;
183
+ before(resetDbAndStartHarness);
184
+ fn(harness);
185
+ after(shutdownTests);
186
+ isInSuite = false;
187
+ });
188
+ },
189
+ describe,
190
+ it: patchedIt,
191
+ };
192
+ options.defineAdditionalTests(args);
193
+ global.it = originalIt;
194
+ });
151
195
  }
152
196
  });
153
197
  }
@@ -21,7 +21,7 @@ export declare class TestHarness extends EventEmitter {
21
21
  * @param testDir The directory the integration tests are executed in
22
22
  */
23
23
  constructor(adapterDir: string, testDir: string, dbConnection: DBConnection);
24
- private adapterName;
24
+ readonly adapterName: string;
25
25
  private appName;
26
26
  private testControllerDir;
27
27
  private testAdapterDir;
@@ -48,9 +48,10 @@ export declare class TestHarness extends EventEmitter {
48
48
  startAdapter(env?: NodeJS.ProcessEnv): Promise<void>;
49
49
  /**
50
50
  * Starts the adapter in a separate process and resolves after it has started
51
+ * @param waitForConnection By default, the test will wait for the adapter's `alive` state to become true. Set this to `true` to wait for the `info.connection` state instead.
51
52
  * @param env Additional environment variables to set
52
53
  */
53
- startAdapterAndWait(env?: NodeJS.ProcessEnv): Promise<void>;
54
+ startAdapterAndWait(waitForConnection?: boolean, env?: NodeJS.ProcessEnv): Promise<void>;
54
55
  /** Tests if the adapter process is still running */
55
56
  isAdapterRunning(): boolean;
56
57
  /** Tests if the adapter process has already exited */
@@ -27,6 +27,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
27
27
  };
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
29
  exports.TestHarness = void 0;
30
+ /* eslint-disable @typescript-eslint/no-inferrable-types */
30
31
  const async_1 = require("alcalzone-shared/async");
31
32
  const objects_1 = require("alcalzone-shared/objects");
32
33
  const child_process_1 = require("child_process");
@@ -157,14 +158,16 @@ class TestHarness extends events_1.EventEmitter {
157
158
  }
158
159
  /**
159
160
  * Starts the adapter in a separate process and resolves after it has started
161
+ * @param waitForConnection By default, the test will wait for the adapter's `alive` state to become true. Set this to `true` to wait for the `info.connection` state instead.
160
162
  * @param env Additional environment variables to set
161
163
  */
162
- async startAdapterAndWait(env = {}) {
164
+ async startAdapterAndWait(waitForConnection = false, env = {}) {
163
165
  return new Promise((resolve, reject) => {
166
+ const waitForStateId = waitForConnection
167
+ ? `${this.adapterName}.0.info.connection`
168
+ : `system.adapter.${this.adapterName}.0.alive`;
164
169
  this.on("stateChange", async (id, state) => {
165
- if (id === `system.adapter.${this.adapterName}.0.alive` &&
166
- state &&
167
- state.val === true) {
170
+ if (id === waitForStateId && state && state.val === true) {
168
171
  resolve();
169
172
  }
170
173
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iobroker/testing",
3
- "version": "2.6.0",
3
+ "version": "3.0.0",
4
4
  "description": "Shared utilities for adapter and module testing in ioBroker",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -38,36 +38,36 @@
38
38
  },
39
39
  "homepage": "https://github.com/AlCalzone/testing#readme",
40
40
  "devDependencies": {
41
- "@alcalzone/release-script": "^3.5.6",
42
- "@alcalzone/release-script-plugin-license": "^3.5.3",
41
+ "@alcalzone/release-script": "^3.5.9",
42
+ "@alcalzone/release-script-plugin-license": "^3.5.9",
43
43
  "@iobroker/adapter-core": "^2.6.0",
44
44
  "@tsconfig/node12": "^1.0.9",
45
- "@types/chai": "^4.3.0",
45
+ "@types/chai": "^4.3.1",
46
46
  "@types/chai-as-promised": "^7.1.5",
47
47
  "@types/debug": "4.1.7",
48
48
  "@types/fs-extra": "^9.0.13",
49
- "@types/iobroker": "^4.0.1",
50
- "@types/mocha": "^9.1.0",
51
- "@types/node": "^16.11.26",
49
+ "@types/iobroker": "^4.0.2",
50
+ "@types/mocha": "^9.1.1",
51
+ "@types/node": "^12.20.50",
52
52
  "@types/sinon": "^10.0.11",
53
53
  "@types/sinon-chai": "^3.2.8",
54
- "@typescript-eslint/eslint-plugin": "^5.17.0",
55
- "@typescript-eslint/parser": "^5.13.0",
56
- "eslint": "^8.12.0",
54
+ "@typescript-eslint/eslint-plugin": "^5.22.0",
55
+ "@typescript-eslint/parser": "^5.22.0",
56
+ "eslint": "^8.15.0",
57
57
  "eslint-config-prettier": "^8.5.0",
58
58
  "eslint-plugin-prettier": "^4.0.0",
59
- "prettier": "^2.6.1",
59
+ "prettier": "^2.6.2",
60
60
  "rimraf": "^3.0.2",
61
61
  "source-map-support": "^0.5.21",
62
- "ts-node": "^10.6.0",
63
- "typescript": "~4.6.2"
62
+ "ts-node": "^10.7.0",
63
+ "typescript": "~4.6.4"
64
64
  },
65
65
  "dependencies": {
66
66
  "alcalzone-shared": "~4.0.1",
67
67
  "chai": "^4.3.6",
68
68
  "chai-as-promised": "^7.1.1",
69
- "debug": "^4.3.3",
70
- "fs-extra": "^10.0.1",
69
+ "debug": "^4.3.4",
70
+ "fs-extra": "^10.1.0",
71
71
  "mocha": "^9.2.2",
72
72
  "sinon": "^13.0.1",
73
73
  "sinon-chai": "^3.7.0"