

ESLint, Typings, Tests 設定
0. 基本介紹
在專案開發中,統一的代碼風格、明確的型別檢查以及穩定的測試流程,能夠有效提升專案的可維護性,並避免因不同開發者的工作環境差異導致不必要的版本問題。以下是我們設定 ESLint、Typings 和測試環境的步驟。
- ESLint 用來保持一致的代碼風格,避免開發過程中不同開發者之間因代碼風格不同而引起的變動。
- Typings 資料夾的設置是為了清晰管理型別定義,讓程式碼更加自我說明,並減少額外文件的依賴。
- Tests 資料夾則是專門用來撰寫測試,確保專案的每個功能都能被有效測試,避免隱藏錯誤。
1. 資料夾結構
做完以後最終的資料夾結構如下:
❯ 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.ts2. ESLint 設定
2.1 安裝 ESLint 及相關套件
使用以下指令安裝 ESLint 和 TypeScript 相關的插件:
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-eslint2.2 初始化 ESLint 配置
使用 eslint --init 來初始化 ESLint 配置(最新版本可能與以往不同,生成 .mjs 格式的配置文件):
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 可能類似於以下內容:
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 的執行指令:
"scripts": {
"lint": "eslint 'src/**/*.ts'"
}這樣,你可以使用 pnpm run lint 來執行代碼檢查。
基本上會有兩個錯誤:
❯ 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 類型:
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.ts 和 src/typings/routes/users.ts
使用以下指令建立 src/typings 資料夾來管理路由相關的型別定義:
mkdir -p src/typings/routes
touch src/typings/routes/index.ts src/typings/routes/users.ts4.2 在 src/typings/routes/index.ts 中定義路由的 enum
// src/typings/routes/index.ts
export enum IndexRoutePaths {
HOME = '/',
USERS = '/users',
}4.3 在 src/typings/routes/users.ts 中定義與 users 路由相關的 enum
// src/typings/routes/users.ts
export enum UserRoutePaths {
INDEX = '/',
}4.4 確保 tsconfig.json 包含 src/typings 和 tests
在 tsconfig.json 中確認 include 部分包含 src/typings/ 和 tests/ 資料夾:
{
"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 來定義路徑:
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 來定義路徑:
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 設定:
pnpm exec jest --init6.2 初始化步驟
選擇以下選項來初始化 Jest 設定:
- Would you like to use Jest when running “test” script in “package.json”?
- 選擇:
Yes
- 選擇:
- Would you like to use Typescript for the configuration file?
- 選擇:
Yes
- 選擇:
- Choose the test environment that will be used for testing
- 選擇:
node
- 選擇:
- Do you want Jest to add coverage reports?
- 選擇:
Yes
- 選擇:
- Which provider should be used to instrument code for coverage?
- 選擇:
v8
- 選擇:
- Automatically clear mock calls, instances, contexts and results before every test?
- 選擇:
No
- 選擇:
最終,Jest 會修改 package.json 並生成 jest.config.ts 文件。
6.3 更新 jest.config.ts
以下是 Jest 的配置內容:
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 測試指令:
"scripts": {
"test": "jest"
}6.5 建立 tests 資料夾
使用以下指令建立 tests/ 資料夾來撰寫測試:
mkdir tests
mkdir tests/routes
touch tests/routes/index.test.ts
touch tests/routes/users.test.ts6.6 撰寫測試文件
在 tests/routes/index.test.ts 中撰寫基本測試:
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 中撰寫基本測試:
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 測試結果
當所有測試通過後,使用以下指令可以運行測試並檢查覆蓋率:
pnpm run test測試成功結果應如下:
❯ 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 編寫測試,但未來可能會考慮撰寫相關測試來覆蓋伺服器啟動的場景。可能的測試項目包括:
- 伺服器成功啟動:確認伺服器可以在指定的端口上啟動。
- 處理端口被佔用的錯誤:模擬
EADDRINUSE錯誤,確保正確處理。 - 處理權限不足的錯誤:模擬
EACCES錯誤,檢查應用是否正確終止。
為什麼測試 www.ts
- 伺服器啟動流程的關鍵性:
- 負責 PORT 配置、HTTP 伺服器創建、錯誤處理邏輯(如
EACCES、EADDRINUSE)。
- 負責 PORT 配置、HTTP 伺服器創建、錯誤處理邏輯(如
- 測試覆蓋率的完整性:
- 提升測試覆蓋率,確保應用穩定。
- 錯誤處理邏輯驗證:
- 檢查伺服器啟動錯誤處理是否正確運作。
為什麼不測試 www.ts
- 重複測試:
- 啟動邏輯已在
app.ts及路由測試中覆蓋。
- 啟動邏輯已在
- 依賴伺服器狀態:
- 測試伺服器端口增加測試複雜度及環境依賴。
建議
- 如果伺服器啟動邏輯重要,測試錯誤處理邏輯是有價值的。
- 如果伺服器邏輯簡單且穩定,可以將測試重點放在應用邏輯上。
可測試的場景
- 伺服器成功啟動。
- 端口被佔用錯誤處理 (
EADDRINUSE)。 - 權限不足錯誤處理 (
EACCES)。
總結
測試 www.ts 能增強應用穩定性,但非必須。