單元測試或整合測試是軟體開發的重點之一,它能夠確保系統所有的服務元件都能如期運作。本次將會介紹測試基本的 Moleculer 應用程式。
這裡使用 Jest[2] 進行測試。你也可以使用其它功能類似的測試框架。
這是一個基本的 Moleculer 服務使用單元測試的框架結構。
const { ServiceBroker } = require("moleculer");
// 讀取服務綱目
const ServiceSchema = require("../../services/<SERVICE-NAME>.service");
describe("Test '<SERVICE-NAME>'", () => {
// 建立一個 ServiceBroker
let broker = new ServiceBroker({ logger: false });
// 建立一個實體服務
let service = broker.createService(ServiceSchema);
// 在測試之前,啟動 Broker 並初始化服務。
beforeAll(() => broker.start());
// 測試完畢後,優雅的停止 Broker
afterAll(() => broker.stop());
/** 在這裡撰寫測試 **/
});
建議在測試時關閉 log ,才不會讓主控台出現雜亂的 log 資訊,你可以在配置檔設定為
logger: false
。
範例:這是一個簡單的服務範例,它會接收一個參數 name
,並且返回大寫的字母 name
。它使用了驗證機制來保證參數 name
是一個字串,以避免 toUpperCase
處理時出現錯誤。另外在執行過程中也會發送一個 name.uppercase
事件。
services/helper.service.js
module.exports = {
name: "helper",
actions: {
toUpperCase: {
// 參數驗證
params: {
name: "string"
},
handler(ctx) {
// 發送一個事件
ctx.emit("name.uppercase", ctx.params.name);
return ctx.params.name.toUpperCase();
}
}
}
};
此範例可以做 3 個測試,分別是輸出的值、是否發送事件、參數驗證。
範例: helper
服務的單元測試。
helper.test.js
const { ServiceBroker, Context } = require("moleculer");
const { ValidationError } = require("moleculer").Errors;
// 讀取 `helper` 服務綱目
const HelperSchema = require("../../services/helper.service");
describe("Test 'helper' actions", () => {
let broker = new ServiceBroker({ logger: false });
let service = broker.createService(HelperSchema);
beforeAll(() => broker.start());
afterAll(() => broker.stop());
describe("Test 'helper.toUpperCase' action", () => {
it("should return uppercase name", async () => {
// 呼叫 Action
const result = await broker.call("helper.toUpperCase", {
name: "John"
});
// 確認結果
expect(result).toBe("JOHN");
});
it("should reject with a ValidationError", async () => {
expect.assertions(1);
try {
await broker.call("helper.toUpperCase", { name: 123 });
} catch (err) {
// 捕獲錯誤,確認是否為 ValidationError
expect(err).toBeInstanceOf(ValidationError);
}
});
it("should emit 'name.uppercase' event ", async () => {
// 監視 context 發送函數
jest.spyOn(Context.prototype, "emit");
// 呼叫 action
await broker.call("helper.toUpperCase", { name: "john" });
// 確認 "emit" 有被呼叫
expect(Context.prototype.emit).toBeCalledTimes(1);
expect(Context.prototype.emit).toHaveBeenCalledWith(
"name.uppercase",
"john"
);
});
});
});
範例:資料庫 Adapters
有時,服務的 Action 會需要接收一些資料並儲存。要測試這類 Action 會需要 Mock 資料庫 Adapter 。這個範例會接收一些參數,然後透過 adapter 來寫入資料庫。
services/users.service.js
const DbService = require("moleculer-db");
module.exports = {
name: "users",
// 讀取資料庫 Adapter
// 它會在 "users" 服務新增 "adapter" 方法
mixins: [DbService],
actions: {
create: {
handler(ctx) {
// 使用 "adapter.insert" 方法來儲存參數資料
return this.adapter.insert(ctx.params);
}
}
}
};
此範例會建立一個 Mock 一個 adapter.insert
方法,然後測試資料是否正確。
範例: users
服務的單元測試。
users.test.js
const { ServiceBroker } = require("moleculer");
const UsersSchema = require("../../services/users.service");
const MailSchema = require("../../services/mail.service");
describe("Test 'users' service", () => {
let broker = new ServiceBroker({ logger: false });
let usersService = broker.createService(UsersSchema);
// 建立一個 mock insert 函數
const mockInsert = jest.fn(params =>
Promise.resolve({ id: 123, name: params.name })
);
beforeAll(() => broker.start());
afterAll(() => broker.stop());
describe("Test 'users.create' action", () => {
it("should create new user", async () => {
// 使用 mock 來替換掉 adapter 的 insert 方法
usersService.adapter.insert = mockInsert;
// 呼叫 action
let result = await broker.call("users.create", { name: "John" });
// 確認結果
expect(result).toEqual({ id: 123, name: "John" });
// 確認 Mock 已被呼叫
expect(mockInsert).toBeCalledTimes(1);
expect(mockInsert).toBeCalledWith({ name: "John" });
});
});
});
由於事件是射後不理的,它們並不會返回任何資料,所以不容易測試。但我們可以測試事件的 內部
行為。關於事件的測試,作者在 Service
類別實作了一個 emitLocalEventHandler
函數,它可以讓我們直接呼叫事件處理函數。
services/helper.service.js
module.exports = {
name: "helper",
events: {
async "helper.sum"(ctx) {
// 呼叫 "sum" 方法
return this.sum(ctx.params.a, ctx.params.b);
}
},
methods: {
sum(a, b) {
return a + b;
}
}
};
範例:helper
服務的單元測試。
helper.test.js
describe("Test 'helper' events", () => {
let broker = new ServiceBroker({ logger: false });
let service = broker.createService(HelperSchema);
beforeAll(() => broker.start());
afterAll(() => broker.stop());
describe("Test 'helper.sum' event", () => {
it("should call the event handler", async () => {
// Mock 一個 "sum" 方法
service.sum = jest.fn();
// 呼叫 "helper.sum" 處理函數
await service.emitLocalEventHandler("helper.sum", { a: 5, b: 5 });
// 確認 "sum" 方法已被呼叫
expect(service.sum).toBeCalledTimes(1);
expect(service.sum).toBeCalledWith(5, 5);
// 還原 "sum" 方法
service.sum.mockRestore();
});
});
});
由於方法是私有的函數,只能在服務中使用。因此你不能從其它服務呼叫它,也不能使用 broker
來執行它。因此,要測試方法時,你只能由服務實例直接來呼叫它們。
services/helper.service.js
module.exports = {
name: "helper",
methods: {
sum(a, b) {
return a + b;
}
}
};
範例:helper
服務的單元測試。
helper.test.js
describe("Test 'helper' methods", () => {
let broker = new ServiceBroker({ logger: false });
let service = broker.createService(HelperSchema);
beforeAll(() => broker.start());
afterAll(() => broker.stop());
describe("Test 'sum' method", () => {
it("should add two numbers", () => {
// 直接呼叫服務的 "sum" 方法
const result = service.sum(1, 2);
expect(result).toBe(3);
});
});
});
本地變數與方法一樣都是私有的,只能在服務中使用。這意味著你只能採取與方法相同的策略來進行測試。
services/helper.service.js
module.exports = {
name: "helper",
/** actions, events, methods **/
created() {
this.someValue = 123;
}
};
範例:helper
服務的單元測試。
helper.test.js
describe("Test 'helper' local variables", () => {
let broker = new ServiceBroker({ logger: false });
let service = broker.createService(HelperSchema);
beforeAll(() => broker.start());
afterAll(() => broker.stop());
it("should init 'someValue'", () => {
expect(service.someValue).toBe(123);
});
});
整合測試可以跨越多個服務,來確保它們之間能夠正常的分工合作。
服務間的依賴很常見,這個例子是 users
服務有一個 notify
action ,它需要依賴 mail
服務的 send
action 來發送 email 。
users.service.js
module.exports = {
name: "users",
actions: {
notify: {
handler(ctx) {
// 依賴 "mail" 服務
return ctx.call("mail.send", { message: "Hi there!" });
}
}
}
};
mail.service.js
module.exports = {
name: "mail",
actions: {
send: {
handler(ctx) {
// 發送 email...
return "Email Sent";
}
}
}
};
範例:users
服務的整合測試。
users.test.js
const { ServiceBroker } = require("moleculer");
const UsersSchema = require("../../services/users.service");
const MailSchema = require("../../services/mail.service");
describe("Test 'users' service", () => {
let broker = new ServiceBroker({ logger: false });
let usersService = broker.createService(UsersSchema);
// 建立一個 "send" 的 mock action
const mockSend = jest.fn(() => Promise.resolve("Fake Mail Sent"));
// 使用 mock 取代 "mail" 服務的 "send" action
MailSchema.actions.send = mockSend;
// 啟動 "mail" 服務
let mailService = broker.createService(MailSchema);
beforeAll(() => broker.start());
afterAll(() => broker.stop());
describe("Test 'users.notify' action", () => {
it("should notify the user", async () => {
let result = await broker.call("users.notify");
expect(result).toBe("Fake Mail Sent");
// 確認 mock 已被呼叫
expect(mockSend).toBeCalledTimes(1);
});
});
});
微服務可以透過 API 閘道器來實現業務邏輯。因此,如同單體式系統的 API ,我們也可以對微服務撰寫整合測試。
這裡使用 SuperTest[3] 進行整合測試。你也可以使用其它功能類似的測試框架。
api.service.js
const ApiGateway = require("moleculer-web");
module.exports = {
name: "api",
mixins: [ApiGateway],
settings: {
port: process.env.PORT || 3000,
routes: [
{
path: "/api",
whitelist: ["**"]
}
]
}
};
users.service.js
module.exports = {
name: "users",
actions: {
status: {
// 在 action 設定對外 API 路徑
rest: "/users/status",
handler(ctx) {
// 確認狀態 ...
return { status: "Active" };
}
}
}
};
範例:users
服務的整合測試。
process.env.PORT = 0; // 使用隨機連接埠來測試
const request = require("supertest");
const { ServiceBroker } = require("moleculer");
// 讀取服務綱目
const APISchema = require("../../services/api.service");
const UsersSchema = require("../../services/users.service");
describe("Test 'api' endpoints", () => {
let broker = new ServiceBroker({ logger: false });
let usersService = broker.createService(UsersSchema);
let apiService = broker.createService(APISchema);
beforeAll(() => broker.start());
afterAll(() => broker.stop());
// 測試服務 API
it("test '/api/users/status'", () => {
return request(apiService.server)
.get("/api/users/status")
.then(res => {
expect(res.body).toEqual({ status: "Active" });
});
});
// 測試無效的服務 API
it("test '/api/unknown-route'", () => {
return request(apiService.server)
.get("/api/unknown-route")
.then(res => {
expect(res.statusCode).toBe(404);
});
});
});
[1] Testing, https://moleculer.services/docs/0.14/testing.html
[2] Jest, https://jestjs.io/
[3] SuperTest, https://github.com/visionmedia/supertest