Skip to content

Commit

Permalink
(#11) Add integration test
Browse files Browse the repository at this point in the history
  • Loading branch information
ambersun1234 committed Oct 25, 2023
1 parent ad86653 commit 61665c8
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 3 deletions.
4 changes: 2 additions & 2 deletions _posts/devops/2022-02-08-devops-github-action.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ math: true
通常 CI 裡面會搭配各種測試\
這些測試方法就讓我們拉出來獨立探討
> 可參考 \
> [DevOps - 單元測試 unit test \| Shawn Hsu](../../devops/devops-unit-test)
<!-- > [DevOps - 整合測試 integration test \| Shawn Hsu](../) -->
> [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)

Expand Down
2 changes: 1 addition & 1 deletion _posts/devops/2022-05-09-devops-unit-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
217 changes: 217 additions & 0 deletions _posts/devops/2023-10-25-devops-integration-test.md
Original file line number Diff line number Diff line change
@@ -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<UserResponse[]> => {
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

<hr>

以上的程式碼你都可以在 [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)

0 comments on commit 61665c8

Please sign in to comment.