iT邦幫忙

2022 iThome 鐵人賽

DAY 29
1

測試

單元測試或整合測試是軟體開發的重點之一,它能夠確保系統所有的服務元件都能如期運作。本次將會介紹測試基本的 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

單元測試

Actions

範例:這是一個簡單的服務範例,它會接收一個參數 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 閘道器來實現業務邏輯。因此,如同單體式系統的 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

家家酒小劇場

  • Otter - 寫測試感覺好花時間QAQ
  • Boxy - 當系統變得複雜時,不寫測試以後可能會花更多時間,但亂寫測試又是另外一個問題了OAO

上一篇
Day 28 : REPL 主控台
下一篇
Day 30 : 架構與部署
系列文
Moleculer 家家酒31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言