cover

概念圖

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.ts

2. 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-eslint

2.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.tssrc/typings/routes/users.ts

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

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

4.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/typingstests

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 --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 的配置內容:

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.ts

6.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 編寫測試,但未來可能會考慮撰寫相關測試來覆蓋伺服器啟動的場景。可能的測試項目包括:

  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. 參考連結

參考連結