在微服務架構中,單元測試和整合測試是確保服務品質和穩定性的關鍵步驟。本文將使用 Node.js、Express.js 和 MongoDB 為例,示範如何使用 Jest 框架編寫有效的單元和整合測試。首先,我們會利用 Mock 技術隔離外部依賴,驗證微服務內部邏輯的正確性。接著,我們將建立測試資料函式庫,模擬真實環境,進行整合測試,確保服務間的互動協作無誤。過程中,我們將使用 Axios 函式庫傳送 HTTP 請求,並驗證微服務的回應。

透過模擬 Express.js 路由和 MongoDB 資料函式庫操作,我們可以驗證微服務的核心業務邏輯。這確保了程式碼的品質,並在早期發現潛在問題。此外,我們還將探討如何模擬 HTTP 請求和回應,以及如何驗證資料函式庫的互動。

Metadata 微服務的單元測試

在本文中,我們將使用 Jest 框架對 metadata 微服務進行單元測試。首先,讓我們來看看微服務的程式碼結構。

Metadata 微服務程式碼

以下是 metadata 微服務的程式碼:

const express = require("express");
const mongodb = require("mongodb");

async function startMicroservice(dbhost, dbname, port) {
  const db = client.db(dbname);
  const app = express();

  app.get("/videos", async (req, res) => {
    const videos = await videosCollection.find().toArray();
    res.json({ videos: videos });
  });

  const server = app.listen(port);

  return {
    close: () => {
      server.close();
      client.close();
    },
    db: db,
  };
}

async function main() {
  const PORT = process.env.PORT;
  const DBHOST = process.env.DBHOST;
  const DBNAME = process.env.DBNAME;
  //...
}

這個微服務使用 Express.js 框架建立了一個 REST API,提供了 /videos 路由來查詢和傳回影片的相關資料。

單元測試

現在,讓我們來寫一些單元測試來驗證這個微服務的功能。以下是測試程式碼:

const request = require("supertest");
const app = require("./index");

describe("Metadata 微服務", () => {
  it("應該傳回影片列表", async () => {
    const response = await request(app).get("/videos");
    expect(response.status).toBe(200);
    expect(response.body.videos).toBeInstanceOf(Array);
  });

  it("應該傳回空列表當沒有影片時", async () => {
    const response = await request(app).get("/videos");
    expect(response.status).toBe(200);
    expect(response.body.videos).toEqual([]);
  });
});

在這些測試中,我們使用 supertest 函式庫來傳送 HTTP 請求到微服務,並驗證傳回的結果。

執行測試

要執行這些測試,我們需要安裝 Jest 框架和相關的依賴項。然後,執行以下命令:

npm install
npm test

這將會執行所有的測試並顯示結果。

結果

如果所有測試都透過了,則表示 metadata 微服務的功能正常。否則,需要根據測試結果來修正程式碼。

圖表翻譯:

  graph LR
    A[傳送請求] --> B[處理請求]
    B --> C[傳回結果]
    C --> D[驗證結果]

這個圖表展示了單元測試的流程:傳送請求、處理請求、傳回結果和驗證結果。

微服務單元測試

單元測試是軟體開發中的一個重要步驟,尤其是在微服務架構中。為了確保微服務的可靠性和穩定性,我們需要對其進行徹底的測試。

建立可測試的微服務

為了使微服務更容易測試,我們需要對其進行一些改造。首先,我們需要建立一個可以傳回微服務物件的函式,這個物件應該包含關閉伺服器和資料函式庫連線的方法。

async function startMicroservice(dbHost, dbName, port) {
  // 建立Express伺服器和資料函式庫連線
  const server = await createServer(port);
  const db = await connectToDatabase(dbHost, dbName);

  // 傳回微服務物件
  return {
    close: async () => {
      // 關閉伺服器和資料函式庫連線
      await server.close();
      await db.close();
    },
  };
}

實作單元測試

現在,我們可以使用 Jest 或 Mocha 等測試框架來實作單元測試。以下是一個簡單的示例:

describe('微服務', () => {
  it('應該能夠啟動和關閉', async () => {
    const microservice = await startMicroservice('localhost', 'mydb', 3000);
    expect(microservice).toHaveProperty('close');
    await microservice.close();
  });

  it('應該能夠處理HTTP請求', async () => {
    const microservice = await startMicroservice('localhost', 'mydb', 3000);
    const response = await request.get('http://localhost:3000/videos');
    expect(response.status).toBe(200);
  });
});

測試優點

透過這種方式對微服務進行單元測試,有以下優點:

  • 提高可靠性:透過對每個函式進行個別測試,可以確保微服務的可靠性和穩定性。
  • 減少bug:單元測試可以幫助我們在早期階段發現和修復bug,從而減少整體開發時間和成本。
  • 提高開發效率:透過對微服務進行模組化設計和測試,可以提高開發效率和團隊合作能力。

單元測試與 Jest

在開始進行單元測試之前,我們需要為依賴專案建立模擬物件(mocks)。在這個例子中,Express 和 MongoDB 是我們的依賴專案。在其他情況下,依賴專案可能會有所不同,例如,在與另一個微服務互動時,我們可能需要模擬 amqp 函式庫以便與 RabbitMQ 進行互動。

以下是測試程式碼的片段(來自 chapter-9/example-2/src/index.test.js),它定義了一個名為「metadata 微服務」的測試套件,包含三個測試。我們將這個檔案命名為 index.test.js,以表明它測試的是 index.js 中的程式碼。隨著我們繼續開發微服務,我們將會有更多類別似的檔案,用於涵蓋微服務中所有程式碼的測試。

測試套件的第一部分致力於為 Express 和 MongoDB 函式庫設定模擬物件。注意使用 jest.fn() 建立模擬函式,以便我們可以檢測函式是否被呼叫,以及傳遞給它的引數。接下來,注意使用 jest.doMock(),它允許我們模擬整個 Node.js 模組。這些工具強大且允許我們在不需要調整正在測試的程式碼的情況下替換 Express 和 MongoDB。

以下是測試程式碼的片段:

describe("metadata 微服務", () => {
  const mockListenFn = jest.fn();
  const mockGetFn = jest.fn();

  jest.doMock("express", () => {
    return () => {
      return {
        listen: mockListenFn,
        get: mockGetFn,
      };
    };
  });

  const mockVideosCollection = {};

  const mockDb = {
    collection: () => {
      return mockVideosCollection;
    },
  };

  const mockMongoClient = {
    db: () => {
      return mockDb;
    },
  };

  jest.doMock("mongodb", () => {
    return {
      MongoClient: mockMongoClient,
    };
  });
});

這個例子比較進階,但我想直接展示與微服務相關的單元測試。如果您難以理解這段程式碼,不要太擔心,只需大致瀏覽並理解哪些部分是用於模擬,哪些部分是用於測試即可。

Metadata 微服務的 Jest 測試

介紹

在本文中,我們將探討如何使用 Jest 框架對 metadata 微服務進行單元測試。這個過程涉及建立 mock 函式、設定 Express 應用程式以及模擬 MongoDB 連線。

建立 Mock 函式

首先,我們需要為 metadata 微服務建立一系列的 mock 函式。這些函式將模擬實際應用程式中使用的函式行為,但不會實際執行任何操作。以下是建立這些 mock 函式的示例:

const mockListen = jest.fn();
const mockGet = jest.fn();
const mockExpress = jest.fn(() => ({
  get: mockGet,
  listen: mockListen,
}));

設定 Express 應用程式

接下來,我們需要設定 Express 應用程式的 mock 物件。這個 mock 物件將被用於模擬 Express 應用程式的行為。

const expressApp = mockExpress();

模擬 MongoDB 連線

為了測試 metadata 微服務,我們還需要模擬 MongoDB 連線。這涉及建立 mock 物件來模擬 MongoDB 客戶端、資料函式庫和集合。

const mockMongoClient = {
  db: jest.fn(() => ({
    collection: jest.fn(() => ({
      find: jest.fn(),
    })),
  })),
};

測試 metadata 微服務

現在,我們可以使用這些 mock 物件來測試 metadata 微服務了。以下是一個簡單的測試示例:

describe('metadata 微服務', () => {
  it('應該成功啟動', async () => {
    await connect();
    expect(mockListen).toHaveBeenCalledTimes(1);
  });

  it('應該成功處理 GET 請求', async () => {
    await expressApp.get('/metadata');
    expect(mockGet).toHaveBeenCalledTimes(1);
  });
});

內容解密:

在上面的程式碼中,我們定義了一個 connect 函式,它傳回一個 mock 的 MongoDB 客戶端物件。然後,我們使用 Jest 的 describeit 函式來定義測試套件和個別測試。

在第一個測試中,我們呼叫 connect 函式並驗證 mockListen 函式是否被呼叫一次。

在第二個測試中,我們呼叫 expressApp.get 方法並驗證 mockGet 函式是否被呼叫一次。

圖表翻譯:

  graph LR
    A[connect 函式] -->|傳回|> B[mock MongoDB 客戶端]
    B -->|啟動|> C[Express 應用程式]
    C -->|處理 GET 請求|> D[mock Get 函式]
    D -->|傳回|> E[測試結果]

在這個圖表中,我們展示了 connect 函式傳回 mock MongoDB 客戶端物件,然後啟動 Express 應用程式。Express 應用程式處理 GET 請求並呼叫 mock Get 函式,最終傳回測試結果。

單元測試微服務

單元測試是確保程式碼每個單元功能正常運作的重要步驟。對於微服務架構,單元測試尤其重要,因為每個服務都是獨立的,需要確保它們之間的互動作用是正確的。

測試 /videos 路由

以下是一個使用 Jest 框架進行單元測試的範例:

const { startMicroservice } = require('./index');

test('/videos 路由檢索資料', async () => {
  await startMicroservice('dbhost', 'dbname', 3000);
  const mockRequest = {};
  const mockJsonFn = jest.fn();

  const mockResponse = {
    json: mockJsonFn,
  };

  const mockRecord1 = {};
  const mockRecord2 = {};

  // 對 MongoDB 的 find 方法進行模擬
  mockVideosCollection.find = () => {
    return {
      toArray: async () => {
        return [mockRecord1, mockRecord2];
      },
    };
  };

  // 取得路由處理函式
  const videosRouteHandler = mockGetFn.mock.calls[0][1];

  // 執行路由處理函式
  await videosRouteHandler(mockRequest, mockResponse);

  // 驗證 json 方法被呼叫一次
  expect(mockJsonFn.mock.calls.length).toEqual(1);

  // 驗證回傳的資料
  expect(mockJsonFn.mock.calls[0][0]).toEqual({
    videos: [mockRecord1, mockRecord2],
  });
});

在這個範例中,我們使用 Jest 的 test 函式定義了一個測試案例。測試案例的目的是驗證 /videos 路由是否能夠正確地檢索資料。

Jest 的自動匯入

您可能會疑惑 jest 變數從哪裡來,因為沒有明確的 require 陳述式匯入它。這是因為 Jest 自動匯入了 jest 變數,所以我們不需要明確地匯入它。

Mocking 與預期

在這個範例中,我們使用 jest.fn()jest.doMock() 建立了模擬物件,以取代 Express 和 MongoDB 的實際實作。Jest 提供了許多其他有用的函式用於模擬和指定測試的預期。

Mermaid 圖表:單元測試流程

  flowchart TD
    A[開始] --> B[建立模擬物件]
    B --> C[定義測試案例]
    C --> D[執行路由處理函式]
    D --> E[驗證回傳的資料]
    E --> F[結束]

圖表翻譯:單元測試流程

這個圖表描述了單元測試的流程。首先,建立模擬物件以取代實際的實作。接下來,定義測試案例以驗證路由處理函式的行為。然後,執行路由處理函式並驗證回傳的資料。最後,結束測試案例。

測試微服務初始化

為了確保微服務的正確性,我們需要對其進行初始化。這一步驟包括設定模擬的Express請求和回應物件,這些物件將被傳遞給Express路由處理器。

模擬Express請求和回應

const mockReq = {};
const mockRes = {
  json: jest.fn()
};

模擬資料函式庫查詢

為了測試路由處理器的功能,我們需要模擢資料函式庫查詢的結果。在這個例子中,我們假設find函式會傳回一些模擬的資料函式庫記錄。

const mockFind = jest.fn().mockReturnValue([
  { id: 1, name: 'Video 1' },
  { id: 2, name: 'Video 2' }
]);

提取路由處理器函式

現在,我們需要提取/videos路由的處理器函式,以便我們可以對其進行測試。

const routeHandler = require('./routeHandler');

執行路由處理器

接下來,我們需要執行路由處理器函式,並傳遞模擬的請求和回應物件。

routeHandler(mockReq, mockRes);

驗證結果

最後,我們需要驗證路由處理器是否正確地呼叫了json函式,並傳回了預期的結果。

expect(mockRes.json).toHaveBeenCalledTimes(1);
expect(mockRes.json).toHaveBeenCalledWith([
  { id: 1, name: 'Video 1' },
  { id: 2, name: 'Video 2' }
]);

圖表翻譯:

  sequenceDiagram
    participant MockReq as 模擬請求
    participant MockRes as 模擬回應
    participant RouteHandler as 路由處理器
    participant MockFind as 模擬資料函式庫查詢

    Note over MockReq,MockRes: 初始化模擬請求和回應
    MockReq->>RouteHandler: 傳遞模擬請求和回應
    RouteHandler->>MockFind: 執行模擬資料函式庫查詢
    MockFind->>RouteHandler: 傳回模擬資料函式庫記錄
    RouteHandler->>MockRes: 呼叫json函式傳回結果

這個測試案例確保了微服務的初始化、路由處理器的功能以及資料函式庫查詢的結果都是正確的。

9.6 單元測試

單元測試是確保程式碼正確性的基本步驟。透過 Jest 這個工具,我們可以輕鬆地對微服務進行單元測試。在這個過程中,我們使用 Mock 的方式替換掉 Express 和 MongoDB,從而避免了對實際資料函式庫和伺服器的存取。

9.6.1 寫測試程式

在開始測試之前,我們需要先寫好測試程式。這些程式會檢查我們的微服務是否能夠正常運作,包括啟動服務和從資料函式庫中讀取資料的功能。

9.6.2 執行測試

執行測試的命令是 npx jestnpm test。這些命令會自動執行我們寫好的測試程式,並且顯示測試結果。如果所有測試都透過了,則表示我們的微服務是正常運作的。

9.6.3 成果

透過單元測試,我們可以確保微服務的每一個部分都能夠正常運作。這是開發可靠的微服務的基礎。雖然這看起來似乎不是什麼了不起的成就,但當我們擁有更多的測試時,就會發現這些測試的重要性。

9.7 整合測試

整合測試是單元測試之後的下一步。它關注於多個模組之間的互動,確保整個微服務能夠正常運作。在整合測試中,我們通常會直接與微服務的 HTTP 介面進行互動,檢查它是否能夠正確地處理請求和傳回相應的回應。

9.7.1 測試程式碼

在進行整合測試時,我們不再使用 Mock 的方式替換掉依賴項。相反,我們會啟動一個真實的資料函式庫,並且載入實際的測試資料。這樣,我們就可以在一個更接近實際環境的情況下進行測試。

9.7.2 執行整合測試

執行整合測試需要一個真實的 MongoDB 資料函式庫。我們可以下載和安裝 MongoDB,以便進行測試。在測試過程中,我們會向微服務傳送 HTTP 請求,並且檢查傳回的回應是否正確。

  flowchart TD
    A[開始] --> B[啟動微服務]
    B --> C[載入測試資料]
    C --> D[傳送HTTP請求]
    D --> E[檢查回應]
    E --> F[結束]

圖表翻譯:

這個流程圖描述了整合測試的過程。首先,我們啟動微服務,然後載入實際的測試資料。接下來,我們傳送 HTTP 請求給微服務,並且檢查傳回的回應是否正確。如果所有步驟都成功了,則表示整合測試透過了。

內容解密:

在整合測試中,我們使用 Jest 作為測試工具。Jest 提供了方便的 API,讓我們可以輕鬆地寫出測試程式並且執行它們。在這個過程中,我們不再使用 Mock 的方式替換掉依賴項,而是直接與微服務的 HTTP 介面進行互動。這樣,我們就可以在一個更接近實際環境的情況下進行測試,從而確保微服務的可靠性和穩定性。

Jest 行程與微服務整合測試

在進行微服務整合測試時,我們可以利用 Jest 行程來執行測試。這個過程包括了建立一個測試資料函式庫、載入測試資料,並進行整合測試。

測試資料函式庫設定

首先,我們需要設定一個測試資料函式庫。在這個例子中,我們使用 MongoDB 作為資料函式庫。若您尚未安裝 MongoDB,可以按照以下步驟進行安裝:

  1. 移至 chapter-9/example-3 目錄。
  2. 執行 docker compose up 啟動 MongoDB 容器。

載入測試資料

接下來,我們需要載入測試資料。測試資料是一組固定的測試資料,用於初始化資料函式庫。為了方便載入測試資料,我們可以建立一個 JavaScript 輔助函式,直接將資料載入 MongoDB 中。

以下是載入測試資料的函式範例:

async function loadDatabaseFixture(collectionName, records) {
  await microservice.db.dropDatabase();
  await collection.insertMany(records);
}

這個函式接受兩個引數:collectionNamerecords。它首先重置資料函式庫,然後插入指定的資料記錄。

Jest 測試指令碼

index.test.js 中,我們可以呼叫這個函式來載入測試資料。以下是範例:

const microservice = require('./index');

describe('Microservice Integration Test', () => {
  beforeEach(async () => {
    await loadDatabaseFixture('myCollection', [
      { name: 'John Doe', age: 30 },
      { name: 'Jane Doe', age: 25 },
    ]);
  });

  it('should perform integration test', async () => {
    // 進行整合測試邏輯
  });
});

在這個範例中,beforeEach 函式在每個測試之前載入測試資料。

建立 Jest 整合測試

建立 Jest 整合測試與建立單元測試非常相似,因為我們不進行任何模擬(mocking),因此簡化了測試程式碼。與其直接呼叫微服務中的程式碼,我們將使用 HTTP 請求來觸發要測試的程式碼。為了傳送 HTTP 請求,我們可以使用 Node.js 中的低階 HTTP 函式庫或透過 npm 安裝的函式庫。在這種情況下,我們將使用 Axios 函式庫,這是一個更現代的函式庫,直接支援 async/await,因此與 Jest 對非同步編碼的支援很好地配合。

首先,確保 Axios 已經增加到 package.json 檔案中。如果您已經安裝了所有 example-3 的依賴項,那麼您已經有了 Axios。否則,您可以在新專案中使用以下命令安裝 Axios:

npm install --save-dev axios

我們這裡使用 --save-dev 引數,因為在這種情況下,我們只會在測試中使用 Axios。因此,它可以作為一個開發依賴項。如果您計劃在生產程式碼中使用 Axios,請確保以常規依賴項的形式安裝它,使用 --save 而不是 --save-dev

以下是整合測試的程式碼示例(chapter-9/example-3/src/index.test.js):

const axios = require("axios");
const mongodb = require("mongodb");

describe("metadata 微服務", () => {
  const DBHOST = "mongodb://localhost:27017";
  const DBNAME = "testdb";
  const { startMicroservice } = require("./index");
  let microservice;

  beforeAll(async () => {
    microservice = await startMicroservice(DBHOST, DBNAME);
  });

  afterAll(async () => {
    await microservice.close();
  });

  function httpGet(route) {
    const url = `${BASE_URL}${route}`;
    return axios.get(url);
  }

  async function loadDatabaseFixture(collectionName, records) {
    await microservice.db.dropDatabase();
    await collection.insertMany(records);
  }

  test("/videos 路由檢索資料", async () => {
    const id1 = new mongodb.ObjectId();
    const id2 = new mongodb.ObjectId();
    const videoPath1 = "my-video-1.mp4";
    const videoPath2 = "my-video-2.mp4";
    const testVideos = [
      {
        _id: id1,
        videoPath: videoPath1
      },
      {
        _id: id2,
        videoPath: videoPath2
      }
    ];

    await loadDatabaseFixture("videos", testVideos);
    const response = await httpGet("/videos");
    expect(response.status).toEqual(200);
    const videos = response.data.videos;
    expect(videos.length).toEqual(2);
    expect(videos[0]._id).toEqual(id1.toString());
    expect(videos[0].videoPath).toEqual(videoPath1);
    expect(videos[1]._id).toEqual(id2.toString());
    expect(videos[1].videoPath).toEqual(videoPath2);
  });
});

這段程式碼定義了一個整合測試,測試 /videos 路由是否能夠正確地檢索資料。它首先啟動微服務,然後載入測試資料到資料函式庫中,最後傳送 HTTP GET 請求到 /videos 路由,並驗證回應是否正確。

注意:請小心不要將這個測試執行在生產資料函式庫上,因為它會刪除整個資料函式庫!請確保只執行它在測試資料函式庫上。

微服務的關鍵元件:資料函式庫連線和測試

在設計微服務時,資料函式庫連線和測試是兩個不可或缺的部分。資料函式庫連線允許微服務存取和操作資料,而測試則確保微服務的正確性和可靠性。

從技術架構視角來看,微服務的單元測試和整合測試對於確保其功能的正確性和穩定性至關重要。本文深入探討瞭如何使用 Jest 框架對 metadata 微服務進行單元和整合測試,涵蓋了 mock 函式的建立、Express 應用程式的設定、MongoDB 連線的模擬,以及測試資料函式庫的設定和資料載入。透過模擬外部依賴項,單元測試驗證了微服務內部邏輯的正確性;而整合測試則在更接近真實環境的條件下,檢驗了微服務與資料函式庫等外部元件的互動是否符合預期。分析顯示,儘管設定測試環境和編寫測試程式碼需要一定的初始投入,但完善的測試機制能有效降低 bug 產生率,提升程式碼品質,並最終節省開發和維護成本。技術限制深析表明,對於複雜的微服務架構,單純的單元測試可能無法完全覆寫所有場景,因此需要結合整合測試,甚至端對端測試來確保整體系統的穩定性。玄貓認為,隨著微服務架構的普及,自動化測試將成為不可或缺的環節,技術團隊應積極探索更高效的測試方法和工具,以提升微服務開發的效率和品質。對於追求高品質和高可靠性的微服務系統,投資於全面的測試策略將帶來長遠的回報。