[express][02]ESLint, Typings, Tests 設定

ESLint, Typings, Tests 設定

0. 基本介紹

在專案開發中,統一的代碼風格、明確的型別檢查以及穩定的測試流程,能夠有效提升專案的可維護性,並避免因不同開發者的工作環境差異導致不必要的版本問題。以下是我們設定 ESLint、Typings 和測試環境的步驟。

  • ESLint 用來保持一致的代碼風格,避免開發過程中不同開發者之間因代碼風格不同而引起的變動。
  • Typings 資料夾的設置是為了清晰管理型別定義,讓程式碼更加自我說明,並減少額外文件的依賴。
  • Tests 資料夾則是專門用來撰寫測試,確保專案的每個功能都能被有效測試,避免隱藏錯誤。

1. 資料夾結構

做完以後最終的資料夾結構如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
❯ tree
.
├── coverage
├── eslint.config.mjs
├── jest.config.ts
├── package.json
├── pnpm-lock.yaml
├── public
│ ├── images
│ ├── index.html
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── src
│ ├── app.ts
│ ├── bin
│ │ └── www.ts
│ └── routes
│ ├── index.ts
│ └── users.ts
├── tests
│ └── routes
│ ├── index.test.ts
│ └── users.test.ts
├── tsconfig.json
└── typings
└── routes
├── index.ts
└── users.ts

2. ESLint 設定

2.1 安裝 ESLint 及相關套件

使用以下指令安裝 ESLint 和 TypeScript 相關的插件:

1
pnpm add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-airbnb-base eslint-config-prettier eslint-plugin-import eslint-plugin-node eslint-plugin-prettier prettier @eslint/js typescript-eslint

2.2 初始化 ESLint 配置

使用 eslint --init 來初始化 ESLint 配置(最新版本可能與以往不同,生成 .mjs 格式的配置文件):

1
pnpm exec eslint --init

如果選項有所不同,以下是新的初始化步驟:

  • How would you like to use ESLint?

    • 選擇:To check syntax and find problems
  • What type of modules does your project use?

    • 選擇:esm
  • Which framework does your project use?

    • 選擇:none
  • Does your project use TypeScript?

    • 選擇:typescript
  • Where does your code run?

    • 選擇:node

ESLint 會根據這些選項生成一個新的配置文件 eslint.config.mjs

2.3 ESLint 配置文件 eslint.config.mjs

生成的 eslint.config.mjs 可能類似於以下內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";

export default [
{ files: ["**/*.{js,mjs,cjs,ts}"] },
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
}
}
];

2.4 在 package.json 中添加 ESLint 指令

package.json 中新增 ESLint 的執行指令:

1
2
3
"scripts": {
"lint": "eslint 'src/**/*.ts'"
}

這樣,你可以使用 pnpm run lint 來執行代碼檢查。
基本上會有兩個錯誤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
❯ pnpm run lint

> express-ts-proto@0.0.0 lint /Users/**yourname**/work/express-ts-proto
> eslint 'src/**/*.ts'


/Users/**yourname**/work/express-ts-proto/src/app.ts
2:10 error 'join' is defined but never used @typescript-eslint/no-unused-vars

/Users/**yourname**/work/express-ts-proto/src/bin/www.ts
27:25 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any

✖ 2 problems (2 errors, 0 warnings)

 ELIFECYCLE  Command failed with exit code 1.

join 的錯誤只需在 app.ts 中移除 join,而另一個錯誤則需要更多的調整。


3. Error Handling 更新

3.1 更新 src/bin/www.ts 中的錯誤處理

src/bin/www.ts 中,將 any 修改為 NodeJS.ErrnoException 以避免使用 any 類型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const onError = (error: NodeJS.ErrnoException) => {
if (error.syscall !== 'listen') {
throw error;
}

const bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;

// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
};

4. Typings 設定

4.1 建立 src/typings/routes/index.tssrc/typings/routes/users.ts

使用以下指令建立 src/typings 資料夾來管理路由相關的型別定義:

1
2
mkdir -p src/typings/routes
touch src/typings/routes/index.ts src/typings/routes/users.ts

4.2 在 src/typings/routes/index.ts 中定義路由的 enum

1
2
3
4
5
6
// src/typings/routes/index.ts

export enum IndexRoutePaths {
HOME = '/',
USERS = '/users',
}

4.3 在 src/typings/routes/users.ts 中定義與 users 路由相關的 enum

1
2
3
4
5
// src/typings/routes/users.ts

export enum UserRoutePaths {
INDEX = '/',
}

4.4 確保 tsconfig.json 包含 src/typingstests

tsconfig.json 中確認 include 部分包含 src/typings/tests/ 資料夾:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"rootDir": "./",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src/**/*", "tests/**/*", "typings/**/*"]
}

5. Routes 設定

5.1 更新 src/routes/index.ts

src/routes/index.ts 中使用 enum 來定義路徑:

1
2
3
4
5
6
7
8
9
10
11
12
import { Router, Request, Response } from 'express';
import usersRouter from './users';
import { IndexRoutePaths } from '../typings/routes/index';

const router = Router();

router.get(IndexRoutePaths.HOME, (req: Request, res: Response) => {
res.send('Welcome to the Home Page');
});
router.use(IndexRoutePaths.USERS, usersRouter);

export default router;

5.2 更新 src/routes/users.ts

src/routes/users.ts 中使用 enum 來定義路徑:

1
2
3
4
5
6
7
8
9
10
11
import { Router, Request, Response } from 'express';
import { UserRoutePaths } from '../typings/routes/users';

const router = Router();

/* GET users listing. */
router.get(UserRoutePaths.INDEX, (req: Request, res: Response) => {
res.send('respond with a resource');
});

export default router;

6. Jest 設定

6.1 使用 Jest 初始化

你可以使用以下指令初始化 Jest 設定:

1
pnpm exec jest --init

6.2 初始化步驟

選擇以下選項來初始化 Jest 設定:

  1. Would you like to use Jest when running “test” script in “package.json”?
    • 選擇:Yes
  2. Would you like to use Typescript for the configuration file?
    • 選擇:Yes
  3. Choose the test environment that will be used for testing
    • 選擇:node
  4. Do you want Jest to add coverage reports?
    • 選擇:Yes
  5. Which provider should be used to instrument code for coverage?
    • 選擇:v8
  6. Automatically clear mock calls, instances, contexts and results before every test?
    • 選擇:No

最終,Jest 會修改 package.json 並生成 jest.config.ts 文件。

6.3 更新 jest.config.ts

以下是 Jest 的配置內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
import type {Config} from 'jest';

const config: Config = {
clearMocks: true,
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts'],
coverageDirectory: "coverage",
coverageProvider: "v8",
preset: 'ts-jest',
testEnvironment: 'node',
};

export default config;

6.4 在 package.json 中添加測試指令

package.json 中添加 Jest 測試指令:

1
2
3
"scripts": {
"test": "jest"
}

6.5 建立 tests 資料夾

使用以下指令建立 tests/ 資料夾來撰寫測試:

1
2
3
4
mkdir tests
mkdir tests/routes
touch tests/routes/index.test.ts
touch tests/routes/users.test.ts

6.6 撰寫測試文件

tests/routes/index.test.ts 中撰寫基本測試:

1
2
3
4
5
6
7
8
9
10
import request from 'supertest';
import app from '../../src/app';

describe('GET /', () => {
it('should return Welcome to the Home Page', async () => {
const res = await request(app).get('/');
expect(res.statusCode).toEqual(200);
expect(res.text).toContain('Welcome to the Home Page');
});
});

tests/routes/users.test.ts 中撰寫基本測試:

1
2
3
4
5
6
7
8
9
10
import request from 'supertest';
import app from '../../src/app';

describe('GET /users', () => {
it('should return Welcome to the Home Page', async () => {
const res = await request(app).get('/users');
expect(res.statusCode).toEqual(200);
expect(res.text).toContain('respond with a resource');
});
});

6.7 測試結果

當所有測試通過後,使用以下指令可以運行測試並檢查覆蓋率:

1
pnpm run test

測試成功結果應如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
❯ pnpm run test

> express-ts-proto@0.0.0 test /Users/**yourname**/work/express-ts-proto
> jest

PASS tests/routes/index.test.ts
GET / 200 1.224 ms - 24
PASS tests/routes/users.test.ts
GET /users 200 1.116 ms - 23
------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------|---------|----------|---------|---------|-------------------
All files | 33.58 | 66.66 | 0 | 33.58 |
src | 100 | 100 | 100 | 100 |
app.ts | 100 | 100 | 100 | 100 |
src/bin | 0 | 0 | 0 | 0 |
www.ts | 0 | 0 | 0 | 0 | 1-89
src/routes | 100 | 100 | 100 | 100 |
index.ts | 100 | 100 | 100 | 100 |
users.ts | 100 | 100 | 100 | 100 |
------------|---------|----------|---------|---------|-------------------

Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.953 s, estimated 2 s
Ran all test suites.

7 測試 www.ts 的計畫

目前還沒有針對 src/bin/www.ts 編寫測試,但未來可能會考慮撰寫相關測試來覆蓋伺服器啟動的場景。可能的測試項目包括:

  1. 伺服器成功啟動:確認伺服器可以在指定的端口上啟動。
  2. 處理端口被佔用的錯誤:模擬 EADDRINUSE 錯誤,確保正確處理。
  3. 處理權限不足的錯誤:模擬 EACCES 錯誤,檢查應用是否正確終止。

為什麼測試 www.ts

  1. 伺服器啟動流程的關鍵性
    • 負責 PORT 配置、HTTP 伺服器創建、錯誤處理邏輯(如 EACCESEADDRINUSE)。
  2. 測試覆蓋率的完整性
    • 提升測試覆蓋率,確保應用穩定。
  3. 錯誤處理邏輯驗證
    • 檢查伺服器啟動錯誤處理是否正確運作。

為什麼不測試 www.ts

  1. 重複測試
    • 啟動邏輯已在 app.ts 及路由測試中覆蓋。
  2. 依賴伺服器狀態
    • 測試伺服器端口增加測試複雜度及環境依賴。

建議

  • 如果伺服器啟動邏輯重要,測試錯誤處理邏輯是有價值的。
  • 如果伺服器邏輯簡單且穩定,可以將測試重點放在應用邏輯上。

可測試的場景

  1. 伺服器成功啟動。
  2. 端口被佔用錯誤處理 (EADDRINUSE)。
  3. 權限不足錯誤處理 (EACCES)。

總結

測試 www.ts 能增強應用穩定性,但非必須。

8. 參考連結

參考連結


[express][02]ESLint, Typings, Tests 設定
https://terryyaowork.github.io/express/web-develop/20241021/3915171955/
作者
Terry Yao
發布於
2024年10月21日
許可協議