From 61665c8ced5a4b1ab619e22c8203966c6d250bca Mon Sep 17 00:00:00 2001 From: Shawn Hsu Date: Wed, 25 Oct 2023 19:12:05 +0800 Subject: [PATCH] (#11) Add integration test --- .../devops/2022-02-08-devops-github-action.md | 4 +- _posts/devops/2022-05-09-devops-unit-test.md | 2 +- .../2023-10-25-devops-integration-test.md | 217 ++++++++++++++++++ 3 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 _posts/devops/2023-10-25-devops-integration-test.md diff --git a/_posts/devops/2022-02-08-devops-github-action.md b/_posts/devops/2022-02-08-devops-github-action.md index ac0d12ebf299..e517c53e3d3f 100644 --- a/_posts/devops/2022-02-08-devops-github-action.md +++ b/_posts/devops/2022-02-08-devops-github-action.md @@ -18,8 +18,8 @@ math: true 通常 CI 裡面會搭配各種測試\ 這些測試方法就讓我們拉出來獨立探討 > 可參考 \ -> [DevOps - 單元測試 unit test \| Shawn Hsu](../../devops/devops-unit-test) - +> [DevOps - 單元測試 Unit Test \| Shawn Hsu](../../devops/devops-unit-test)\ +> [DevOps - 整合測試 Integration Test \| Shawn Hsu](../../devops/devops-integration-test) 而實務上來說 CI 就是負責執行以上的事物(包括但不限於 security check, code coverage, functional test and custom check) diff --git a/_posts/devops/2022-05-09-devops-unit-test.md b/_posts/devops/2022-05-09-devops-unit-test.md index 3badd7d5fbc7..8425aae90e35 100644 --- a/_posts/devops/2022-05-09-devops-unit-test.md +++ b/_posts/devops/2022-05-09-devops-unit-test.md @@ -377,7 +377,7 @@ unit test 的宗旨我們開頭有提過,是測試 function 的 `logic`\ 透過不斷的新增修改,你的實作最後會符合所有的需求場景,到此你的功能就開發完成了\ 而開發途中產生的 test case 可以當作 unit test 或 integration test 放在 CI/CD pipeline 上面執行,確保每一次的修改都是符合預期的 -> 有關 integration test 的介紹可以參考 [DevOps - 整合測試 integration test \| Shawn Hsu](../)\ +> 有關 integration test 的介紹可以參考 [DevOps - 整合測試 Integration Test \| Shawn Hsu](../../devops/devops-integration-test)\ > 有關 CI/CD pipeline 的介紹可以參考 [DevOps - 從 GitHub Actions 初探 CI/CD \| Shawn Hsu](../../devops/devops-github-action) ## Struggles to Run TDD diff --git a/_posts/devops/2023-10-25-devops-integration-test.md b/_posts/devops/2023-10-25-devops-integration-test.md new file mode 100644 index 000000000000..05e4ff045157 --- /dev/null +++ b/_posts/devops/2023-10-25-devops-integration-test.md @@ -0,0 +1,217 @@ +--- +title: DevOps - 整合測試 Integration Test +date: 2023-10-25 +categories: [devops] +tags: [integration test, mock, e2e test, docker] +math: true +--- + +# Introduction to Integration Test +光是擁有 unit test,其實是不夠的\ +因為 unit test 測試的範圍只有 function 本身\ +跨 function 之間的整合,是沒有涵蓋到的 + +> 有關 unit test 的部份,可以參考 [DevOps - 單元測試 Unit Test \| Shawn Hsu](../../devops/devops-unit-test) + +integration test 在這方面可以很好的 solution\ +顧名思義,整合測試即為 **整合不同的 component**,一起進行測試 + +> integration test 並沒有一定要跟資料庫一起測試\ +> 多 function 之間的整合也可以用 integration test\ +> 只不過我們時常在 integration test 裡面測試資料庫 + +# Integration Test Scope +那麼究竟有哪些值得測試的呢?\ +所有的狀況都要測試嗎? + +以往在寫 unit test 的時候,為了要包含所有的測試條件\ +我們會將 test case 盡可能的寫完整,寫的很詳盡\ +但是在 integration test 這裡我們不推薦這樣做\ +不在 integration test 做不代表我們不重視那些測試條件\ +我舉個例子好了 + +你有一個 API `POST /group/{groupId}/members`\ +作用是新增 member 到特定的 group 裡面\ +他的架構是這樣子的 +1. input validation middleware 會先檢查輸入是否合法,e.g. `groupId` 必須要是數字 +2. permission middleware 會先檢查使用者是否為 group 的一員,並確認他的權限能否有新增操作的權限 +3. service layer 會準備所需的寫入格式 +4. database layer 則負責執行寫入的操作 + +integration test 會需要做到 input validation 嗎?\ +這些是不是可以在 unit test 寫一個獨立的 validation test 呢? + +因為 integration test 相比 unit test 來說是 heavy 了許多\ +因此我們會希望測試的內容著重在 **component 之間的整合**\ +比如說 `permission middleware + service` 有沒有正常運作, `service + database` 有沒有正常寫入 + +# Dependency Injection +我們在 [DevOps - 單元測試 Unit Test \| Shawn Hsu](../../devops/devops-unit-test) 有提到\ +依賴於實做與依賴於界面的優缺點\ +在實做整合測試的時候,這些 pros and cons 會更大程度的影響你的測試撰寫 + +## Parallel vs. Sequential Execution of Integration Test +講一個例子\ +前陣子我發現公司的專案在執行 integration test 的時候會有問題\ +看到最後發現是 transaction 的問題\ +過程大概是這樣子的 + +Jest 本身在執行測試的時候,為了節省執行時間提昇效率\ +它會使用本機所有的 core\ +根據 [Jest 29.7 --maxWorkers](https://jestjs.io/docs/cli#--maxworkersnumstring) 所述 +> Alias: -w. Specifies the maximum number of workers the worker-pool will spawn for running tests. \ +> In single run mode, this defaults to the number of the cores available on your machine minus one for the main thread. + +而正是因為這個原因,導致我們的測試出了問題\ +原因是我們並沒有做好 transaction 的管理,加上多個 test 同步執行\ +不同測試讀到其他人的結果,進而導致測試失敗\ +我們最後只能犧牲多核的好處,強制讓其用 serializable 的模式下去跑\ +現在我們的測試執行時間長達一分鐘\ +而相對的解決辦法就是使用 Dependency Injection\ +每一次 new 我們的 service 的時候,都給它一個新的 transaction connection\ +如此一來就可以解決上述的問題 + +## Dependency Injection in JavaScript? +寫過一段時間的 JS 的你可能會發現\ +多數的 code 都是採用 functional programming 的方式,很少會使用 Class\ +沒有 Class 要怎麼做 Dependency Injection, 怎麼寫測試呢? + +這時候就必須要用到 Test Double 裡面的 Fake Object 了\ +Fake Object 可以提供較為簡單版本的實做\ +假設你需要替換掉資料庫的界面實做,你可以透過 Fake Object 來做測試\ +如此一來你不需要大改你原本的實做,只需要換成 Fake Object 就可以了 + +> 有關 Test Double 的介紹可以參考 [DevOps - 單元測試 Unit Test \| Shawn Hsu](../../devops/devops-unit-test/#test-double) + +# Docker Container +要測試與資料庫的整合,勢必要起一個 local database\ +我們的老朋友 Docker 就可以派上用場了 + +基本上使用說實在的也沒什麼特別的\ +僅須起個 database 放著,測試的時候呼叫對應的 ip, port 即可\ +就像下面這樣 + +```shell +$ docker run -d --name test \ + -p 6630:3306 \ + -v rest-mysql:/var/lib/mysql \ + -e MYSQL_DATABASE=db \ + -e MYSQL_USER=root \ + -e MYSQL_ROOT_PASSWORD=root \ + mariadb +``` + +# Example +架構上跟 unit test 一樣\ +我們都是寫一個 `describe` block 然後裡面放上我們要測試的東西\ +就像這樣 +```js +describe("test getUsersSlow", () => { + let conn: PrismaClient; + + beforeEach(async () => { + jest.resetAllMocks(); + conn = new PrismaClient(); + jest.spyOn(database, "newConnection").mockReturnValue(conn); + }); + + afterEach(async () => { + await conn.$disconnect(); + }); + + it("should get 2 users from page 1", async () => { + const result = await userService.getUsersSlow(1, 2); + const expectedResult = [ + { + id: 1, + username: "fZAxGMLFJU", + created_at: new Date("2024-04-26T03:39:52.000Z"), + updated_at: new Date("2023-10-23T09:58:36.034Z"), + }, + { + id: 2, + username: "LnJhEZFRlu", + created_at: new Date("2025-06-07T01:21:27.000Z"), + updated_at: new Date("2023-10-23T09:58:36.034Z"), + }, + ]; + + expect(result).toEqual(expectedResult); + }); +}); +``` + +原本的實做是這樣的 +```js +export default { + getUsersSlow: async ( + pageNumber: number, + pageLimit: number + ): Promise => { + let result: UserResponse[] = []; + + try { + const connection = newConnection(); + result = await userDB.findUsersSlow(connection, pageNumber, pageLimit); + logger.info("Successfully get users"); + } catch (error) { + logger.error("Encounter error, abort", { + error: error, + }); + throw new Error(Errors.InternalServerError); + } + + return result; + } +} +``` + +注意到一件事情\ +因為我們在這裡有跟 database 交互\ +為了讓每個測試有獨立的 connection,在 `beforeEach` 的時候手動建立一個 connection\ +並且使用 jest 的 spyOn 功能,將 `database.newConnection` 設置為 `conn`\ +如此一來在測試的時候,就會換成我們的實做了\ +然後每次執行的時候記得要將之前的 mock 重置 :arrow_right: `jest.resetAllMocks()` + +> 呼應到上述 [Dependency Injection in JavaScript?](#dependency-injection-in-javascript) 說到的\ +> 不一定需要使用 Dependency Injection 才能反轉依賴 + +至於測試最後 `afterEach` 為什麼要手動 disconnect?\ +原因是我想要讓每個測試都有獨立的 connection + +## Jest Error +``` +A worker process has failed to exit gracefully and has been force exited. +This is likely caused by tests leaking due to improper teardown. +Try running with --detectOpenHandles to find leaks. +Active timers can also cause this, ensure that .unref() was called on them. +``` + +在寫 mock 的時候要注意到你的 async/await 有沒有寫好\ +或者是說他有沒有正確的 mock 上去\ +如果沒有寫好要在檢查一遍你的 mock + +
+ +以上的程式碼你都可以在 [ambersun1234/blog-labs/cursor-based-pagination](https://github.com/ambersun1234/blog-labs/tree/master/cursor-based-pagination) 當中找到 + +# Difference with E2E Testing +我一開始看到 E2E test 的時候還以為他是跟 integration test 一樣的東西\ +這個字詞比較常在 frontend 的領域看到,不過他的概念在 backend 也同樣適用 + +E2E 的全名是 End 2 End,也就是端到端\ +這裡的端指的是 **使用者端** 與 **服務端**\ +因此 E2E 要測試的範圍,是從使用者的角度來使用我們的服務\ +換句話說,是從 API endpoint 進來到 response 的過程 + +# Comparison of Testing Methodology + +||Unit Test|Integration Test|E2E Test| +|:--|:--:|:--:|:--:| +|Scope|Function|Component|Whole Flow| +|Test Data|Mock|Simulate Data|Real Data| +|Speed|Fast|Slower than Unit Test|Slowest| +|Execution Environment|Local|Local or Staging|Production| + +# References ++ [bard](https://bard.google.com/chat)