Skip to content

Commit

Permalink
feat: add test code
Browse files Browse the repository at this point in the history
  • Loading branch information
nana4rider committed Jan 18, 2025
1 parent 1aa1113 commit 54bddc0
Show file tree
Hide file tree
Showing 12 changed files with 810 additions and 540 deletions.
11 changes: 11 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
MQTT_BROKER=mqtt://localhost
MQTT_USERNAME=username
MQTT_PASSWORD=password
LOG_LEVEL=info
MQTT_TASK_INTERVAL=100
HA_DISCOVERY_PREFIX=homeassistant
PORT=3000
AUTO_REQUEST_INTERVAL=100
WISUN_CONNECTOR_MODEL=BP35C2
ROUTE_B_ID=id
ROUTE_B_PASSWORD=password
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ ECHONET Liteプロトコルを使用して、Wi-SUN対応スマートメータ

## 使い方

必要な環境変数については[こちら](https://github.com/nana4rider/wisun2mqtt/blob/main/src/env.ts)をご確認ください。

### Production

```sh
Expand All @@ -42,6 +44,9 @@ npm run dev
### Docker

```sh
# PAN情報をホスト側に配置するとスキャンを省略し、次回からの接続が早くなります。
touch .paninfo

docker run -d \
--name wisun2mqtt \
--device /dev/ttyUSB0:/dev/ttyUSB0 \
Expand Down
80 changes: 80 additions & 0 deletions __tests__/manager/availabilityManager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Entity } from "@/entity";
import { setupAvailability } from "@/manager/availabilityManager";
import { MqttClient } from "@/service/mqtt";

describe("setupAvailability", () => {
const deviceId = "deviceId1";
let mockMqttClient: jest.Mocked<MqttClient>;
let entities: Entity[];

beforeEach(() => {
mockMqttClient = {
publish: jest.fn(),
} as unknown as jest.Mocked<MqttClient>;

entities = [
{ id: "entity1", name: "Entity 1" },
{ id: "entity2", name: "Entity 2" },
] as Entity[];
});

afterEach(() => {
jest.clearAllTimers();
});

it("pushOnline を呼び出すと全てのエンティティにオンライン状態を送信する", () => {
const { pushOnline, close } = setupAvailability(
deviceId,
entities,
mockMqttClient,
);

pushOnline();

expect(mockMqttClient.publish).toHaveBeenCalledTimes(entities.length);
entities.forEach((entity) => {
expect(mockMqttClient.publish).toHaveBeenCalledWith(
expect.stringContaining(entity.id),
"online",
);
});

close();
});

it("close を呼び出すと全てのエンティティにオフライン状態を送信する", () => {
jest.useFakeTimers();
const { close } = setupAvailability(deviceId, entities, mockMqttClient);

close();

expect(mockMqttClient.publish).toHaveBeenCalledTimes(entities.length);
entities.forEach((entity) => {
expect(mockMqttClient.publish).toHaveBeenCalledWith(
expect.stringContaining(entity.id),
"offline",
);
});
});

it("定期的にオンライン状態を送信する", () => {
jest.useFakeTimers();
const { close } = setupAvailability(deviceId, entities, mockMqttClient);

jest.advanceTimersByTime(10000); // Assume AVAILABILITY_INTERVAL is 10000ms

expect(mockMqttClient.publish).toHaveBeenCalledTimes(entities.length);
entities.forEach((entity) => {
expect(mockMqttClient.publish).toHaveBeenCalledWith(
expect.stringContaining(entity.id),
"online",
);
});

jest.advanceTimersByTime(10000);

expect(mockMqttClient.publish).toHaveBeenCalledTimes(entities.length * 2);

close();
});
});
122 changes: 122 additions & 0 deletions __tests__/service/mqtt.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import initializeMqttClient from "@/service/mqtt";
import mqttjs, { MqttClient } from "mqtt";
import { setTimeout } from "timers/promises";

// 必要なモック関数
const mockSubscribeAsync = jest.fn();
const mockPublishAsync = jest.fn();
const mockEndAsync = jest.fn();
const mockOn = jest.fn<
ReturnType<MqttClient["on"]>,
Parameters<MqttClient["on"]>
>();

jest.mock("mqtt", () => {
return {
connectAsync: jest.fn(),
};
});

beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
});

describe("initializeMqttClient", () => {
test("MQTTクライアントが正常に接続される", async () => {
const mockConnectAsync = mqttjs.connectAsync as jest.Mock;
mockConnectAsync.mockResolvedValue({
subscribeAsync: mockSubscribeAsync,
publishAsync: mockPublishAsync,
endAsync: mockEndAsync,
on: mockOn,
});

const mqtt = await initializeMqttClient();

await mqtt.close();

// MQTTクライアントの接続確認
expect(mockConnectAsync).toHaveBeenCalledWith(
process.env.MQTT_BROKER,
expect.objectContaining({
username: process.env.MQTT_USERNAME,
password: process.env.MQTT_PASSWORD,
}),
);
});

test("publishがタスクキューに追加される", async () => {
const mockConnectAsync = mqttjs.connectAsync as jest.Mock;
mockConnectAsync.mockResolvedValue({
subscribeAsync: mockSubscribeAsync,
publishAsync: mockPublishAsync,
endAsync: mockEndAsync,
on: mockOn,
});

const mqtt = await initializeMqttClient();

// publishを呼び出す
mqtt.publish("topic/publish", "test message", { retain: true });

// タスクキューの状態を確認
expect(mqtt.taskQueueSize).toBe(1);

await mqtt.close(true);
});

test("close(true)を呼び出すとタスクキューが空になりクライアントが終了する", async () => {
const mockConnectAsync = mqttjs.connectAsync as jest.Mock;
mockConnectAsync.mockResolvedValue({
subscribeAsync: mockSubscribeAsync,
publishAsync: mockPublishAsync,
endAsync: mockEndAsync,
on: mockOn,
});
mockPublishAsync.mockImplementation(async () => {
await setTimeout(100);
return Promise.resolve();
});

const mqtt = await initializeMqttClient();

mqtt.publish("topic", "message");

// closeを呼び出す
await mqtt.close(true);

// タスクキューが空になっていることを確認
expect(mqtt.taskQueueSize).toBe(0);

// MQTTクライアントの終了を確認
expect(mockEndAsync).toHaveBeenCalledTimes(1);
});

test("close()を呼び出すとタスクキューが残っていてもクライアントが終了する", async () => {
const mockConnectAsync = mqttjs.connectAsync as jest.Mock;
mockConnectAsync.mockResolvedValue({
subscribeAsync: mockSubscribeAsync,
publishAsync: mockPublishAsync,
endAsync: mockEndAsync,
on: mockOn,
});
mockPublishAsync.mockImplementation(async () => {
await setTimeout(100);
return Promise.resolve();
});

const mqtt = await initializeMqttClient();

mqtt.publish("topic", "message");

// closeを呼び出す
await mqtt.close();

// タスクキューが空になっていないことを確認
expect(mqtt.taskQueueSize).toBe(1);

// MQTTクライアントの終了を確認
expect(mockEndAsync).toHaveBeenCalledTimes(1);
});
});
16 changes: 16 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as fs from "fs";
import { pathsToModuleNameMapper } from "ts-jest";

const tsconfig = JSON.parse(fs.readFileSync("./tsconfig.json", "utf-8"));

/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
testEnvironment: "node",
moduleNameMapper: pathsToModuleNameMapper(tsconfig.compilerOptions.paths, {
prefix: "<rootDir>/",
}),
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
transform: {
"^.+\\.(t|j)sx?$": "@swc/jest",
},
};
39 changes: 39 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import env from "@/env";
import { config } from "dotenv";
import { Writable } from "type-fest";

config({ path: "./.env.test" });

export type MutableEnv = Partial<Writable<typeof env>>;

let overrideEnv: MutableEnv = {};

beforeEach(() => {
overrideEnv = {};
});

jest.mock("@/env", () => {
const { default: defaultEnv } = jest.requireActual<{ default: typeof env }>(
"@/env",
);
return new Proxy(
{ ...defaultEnv },
{
get(target, prop: keyof typeof env) {
if (prop in overrideEnv) {
return overrideEnv[prop];
}
return target[prop];
},
set(
target,
prop: keyof typeof env,
value: (typeof env)[keyof typeof env],
) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
(overrideEnv as any)[prop] = value;
return true;
},
},
);
});
Loading

0 comments on commit 54bddc0

Please sign in to comment.