Install any skill in seconds. Free to start, no credit card required.
Get Started Free →使用 Jest 模擬 Node.js 內建模組(如 fs、path 等)的技能。當測試需要隔離檔案系統操作或進行安全的測試時使用此技能。啟動條件:(1) Mock fs 模組 / Mock fs module (2) Jest Mock 檔案系統 / Jest Mock file system (3) 模擬 Node.js 內建模組 / Mock Node.js built-in modules (4) jest.mock 使用教學 / jest.mock usage guide (5) 使用 memfs-extra 模擬 fs / Use memfs-extra to mock fs (6) 記憶體檔案系統測試 / In-memory file system testing (7) Mock fs 測試 / Mock fs test (8) 隔離檔案系統測試 / Isolated file system testing (9) 安全的檔案系統操作 / Safe file system operations (10) Jest 虛擬檔案 / Jest virtual file syst
.claude/skills/bluelovers-test-js-mock/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 65% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 143% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 88% | 0% |
本技能提供使用 Jest 模擬 Node.js 內建模組的完整指南,特別是針對 fs 檔案系統模組的 Mock 技術。這對於在測試環境中隔離檔案系統操作、避免污染真實檔案系統非常重要。
This skill provides a complete guide to mocking Node.js built-in modules using Jest, especially for the fs file system module. This is crucial for isolating file system operations in test environments and avoiding pollution of the real file system.
在測試中直接操作真實檔案系統會導致以下問題:
Using real file system in tests can cause:
透過在 __mocks__ 資料夾中建立模擬檔案來實現。這是一種全域性的模擬方式,適合於整個專案多個測試檔案都需要模擬 fs 的情況。
This method is implemented by creating a mock file in the __mocks__ folder. It is a global mocking approach suitable for scenarios where multiple test files across the project need to mock fs.
步驟 1:建立模擬檔案
建立 test/__mocks__/fs.js(注意:必須是 .js 副檔名才有效):
javascript// test/__mocks__/fs.js module.exports = require('memfs-extra/fs-extra');
步驟 2:在測試檔案中啟用模擬
typescript// test/some-feature.spec.ts import fs from 'fs'; // 啟動模擬(放在 import 之後) jest.mock('fs'); describe('Some Feature', () => { it('should read file from memory', () => { // 現在 fs 是記憶體中的虛擬檔案系統 expect(fs).toHaveProperty('readJSON'); }); });
此方法在個別測試檔案中直接定義模擬行為。它提供了更高的靈活性,允許你針對特定測試檔案自定義模擬內容。
This method defines the mock behavior directly within individual test files. It provides higher flexibility, allowing you to customize the mock for specific test files.
直接在使用測試檔案的頂部使用 jest.mock 並提供工廠函式:
typescript// test/some-feature.spec.ts import fs from 'fs'; // Mock fs 模組 jest.mock('fs', () => { return require('memfs-extra/fs-extra'); }); // Mock fs/promises 子模組 jest.mock('fs/promises', () => { return require('memfs-extra/fs-extra').promises; }); describe('Some Feature', () => { it('should read file from memory', () => { expect(fs).toHaveProperty('readJSON'); }); });
fs 的測試檔案fs/promises)fstypescript// test/__mocks__/fs.js module.exports = require('memfs-extra/fs-extra');
typescript// test/file-service.spec.ts // @noUnusedParameters:false import fs from 'fs'; import { getVolumeFromFs } from 'memfs-extra'; // 啟動模擬 jest.mock('fs'); describe('FileService', () => { // 驗證 mock 是否成功,失敗時會拋出錯誤 const vol = getVolumeFromFs(fs); expect(vol).toBeDefined(); it('should read JSON file', () => { const testData = { name: 'test' }; // 寫入虛擬檔案 fs.writeFileSync('/test/data.json', JSON.stringify(testData)); // 讀取虛擬檔案 const result = fs.readJSONSync('/test/data.json'); expect(result).toEqual(testData); }); });
typescript// test/file-service-inline.spec.ts // @noUnusedParameters:false import fs from 'fs'; import { getVolumeFromFs } from 'memfs-extra'; // Mock fs 和 fs/promises jest.mock('fs', () => require('memfs-extra/fs-extra')); jest.mock('fs/promises', () => require('memfs-extra/fs-extra').promises); describe('FileService (Inline)', () => { // 驗證 mock 是否成功,失敗時會拋出錯誤 const vol = getVolumeFromFs(fs); expect(vol).toBeDefined(); it('should write and read JSON', () => { const testData = { name: 'test', value: 123 }; fs.writeJSONSync('/test/data.json', testData); const result = fs.readJSONSync('/test/data.json'); expect(result).toEqual(testData); }); it('should handle async operations', async () => { await fs.promises.writeFile('/test/async.txt', 'hello'); const content = await fs.promises.readFile('/test/async.txt', 'utf-8'); expect(content).toBe('hello'); }); });
| 特性 / Feature | Share Mock | Inline Mock | | :--- | :--- | :--- | | 定義位置 / Location | __mocks__/fs.js | 測試檔案內 / Inside test file | | 影響範圍 / Scope | 全域 / 多個檔案 | 單一檔案 | | 配置複雜度 / Complexity | 低(一次性配置) | 中(每個檔案需寫一次) | | 靈活性 / Flexibility | 低 | 高 | | 子模組支援 / Sub-module support | 需額外配置 | 可分別 mock |
typescriptjest.mock('fs', () => require('memfs-extra/fs-extra')); jest.mock('fs/promises', () => require('memfs-extra/fs-extra').promises); jest.mock('path', () => require('path'));
⚠️ 警告:除非有必要才使用這個做法
此方法會覆寫 memfs-extra 的原生行為,可能導致預期外的問題。建議優先使用下方的「虛擬檔案結構設定」方法。
typescript// ⚠️ 僅在有特殊需求時使用 jest.mock('fs', () => { const memfs = require('memfs-extra/fs-extra'); // 自定義行為 const customFs = { ...memfs, // 覆寫特定方法 readFileSync: jest.fn((path) => { if (path.includes('protected')) { throw new Error('Access denied'); } return memfs.readFileSync(path); }), }; return customFs; });
使用 getVolumeFromFs 取得的 Volume 物件來設定虛擬檔案結構,這是設定測試資料的推薦方式:
typescriptimport fs from 'fs'; import { getVolumeFromFs } from 'memfs-extra'; jest.mock('fs', () => require('memfs-extra/fs-extra')); describe('Virtual File Structure', () => { // 驗證 mock 是否成功 const vol = getVolumeFromFs(fs); expect(vol).toBeDefined(); it('should setup virtual file structure', () => { // 方法一:使用 mkdirSync 建立目錄 vol.mkdirSync('/test-dir'); vol.writeFileSync('/test-dir/file.txt', 'content'); // 方法二:使用 appendFileSync 新增檔案 vol.appendFileSync('/another-file.txt', 'hello'); // 方法三:使用 fromJSON 一次設定多個檔案(推薦) vol.fromJSON({ '/config.json': JSON.stringify({ name: 'test' }), '/data/users.json': JSON.stringify([{ id: 1 }]), '/logs/app.log': '2024-01-01 INFO: Started\n', }); // 驗證檔案存在 expect(fs.existsSync('/config.json')).toBe(true); expect(fs.existsSync('/data/users.json')).toBe(true); }); });
Volume API 常用方法:
| 方法 | 說明 | |------|------| | vol.mkdirSync(path) | 建立目錄 | | vol.writeFileSync(path, content) | 寫入檔案 | | vol.appendFileSync(path, content) | 追加檔案內容 | | vol.fromJSON(object) | 從物件建立多個檔案 | | vol.readFileSync(path) | 讀取檔案 | | vol.rmSync(path, { recursive: true }) | 刪除檔案/目錄 |
當需要同時使用虛擬檔案系統和真實檔案時:
typescriptimport * as fs from 'fs'; import * as realFs from 'fs'; jest.mock('fs', () => { const memfs = require('memfs-extra/fs-extra'); return { ...memfs, // 保持真實 fs 的某些方法 existsSync: realFs.existsSync, }; });
⚠️ 重要:即使有 mock 檔案系統,仍須遵守路徑安全原則
即使使用 memfs-extra 模擬了 fs 模組,仍應確保所有檔案操作都限制在測試臨時目錄內,避免路徑有機會離開臨時目錄範圍。
禁止使用的路徑模式:
/、C:\)正確的做法:
typescript// __root.ts import { join } from 'path'; /** 專案根目錄 / Project root directory */ export const __ROOT = join(__dirname, '..'); /** 測試目錄路徑 / Test directory path */ export const __ROOT_TEST = join(__ROOT, 'test'); /** 測試臨時目錄路徑 / Test temporary directory path */ export const __ROOT_TEST_TEMP = join(__ROOT_TEST, 'temp');
typescriptimport { join } from 'path'; import { __ROOT_TEST_TEMP } from '../__root'; // ✅ 正確:使用測試臨時目錄 const testSettingsPath = join(__ROOT_TEST_TEMP, 'mock/settings'); const testSettingsJsonPath = join(testSettingsPath, 'settings.json'); // ❌ 錯誤:使用根路徑 const unsafePath = '/test/config.json';
typescriptimport fs from 'fs'; import { getVolumeFromFs } from 'memfs-extra'; import { __ROOT_TEST_TEMP } from '../__root'; jest.mock('fs', () => require('memfs-extra/fs-extra')); const vol = getVolumeFromFs(fs); // ✅ 正確:使用臨時目錄相對路徑(在虛擬環境中) vol.mkdirSync(join(__ROOT_TEST_TEMP, 'mock')); vol.writeFileSync(join(__ROOT_TEST_TEMP, 'mock/settings.json'), '{}'); // ❌ 錯誤:使用根路徑(可能覆蓋真實系統檔案) vol.writeFileSync('/etc/config.json', '{}');
typescriptdescribe('Safe File Operations', () => { const testDir = '/test/temp'; beforeEach(() => { // 每個測試前清空虛擬目錄 if (fs.existsSync(testDir)) { fs.rmSync(testDir, { recursive: true }); } }); it('should isolate test operations', () => { fs.mkdirSync(testDir, { recursive: true }); fs.writeFileSync(`${testDir}/test.txt`, 'content'); expect(fs.existsSync(`${testDir}/test.txt`)).toBe(true); }); });
.js 副檔名?Jest 的自動 mock 機制需要 .js 副檔名才能正確識別模擬模組。
使用相同的方式:
typescriptjest.mock('path', () => require('path')); jest.mock('os', () => require('os'));
將 jest.mock('fs') 放在測試檔案的頂部,確保在任何測試執行前就已載入。
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 14,623 | 8,967 | -39% | 1 | 1 | 0% | 3,211 | 5,348 | +67% | 0 | 0 | — |
case-02 | fail→fail | 17,324 | 10,622 | -39% | 1 | 1 | 0% | 3,616 | 5,517 | +53% | 0 | 0 | — |
case-03 | fail→pass | 17,768 | 12,273 | -31% | 1 | 1 | 0% | 3,710 | 6,111 | +65% | 0 | 0 | — |
case-04 | fail→pass | 12,654 | 5,793 | -54% | 1 | 1 | 0% | 2,450 | 4,732 | +93% | 0 | 0 | — |
case-05 | fail→fail | 8,318 | 4,011 | -52% | 1 | 1 | 0% | 1,489 | 4,123 | +177% | 0 | 0 | — |
case-06 | fail→pass | 9,562 | 7,998 | -16% | 1 | 1 | 0% | 1,933 | 4,704 | +143% | 0 | 0 | — |
case-07 | fail→pass | 13,461 | 8,731 | -35% | 1 | 1 | 0% | 2,777 | 5,234 | +88% | 0 | 0 | — |
case-08 | fail→fail | 14,120 | 13,694 | -3% | 1 | 1 | 0% | 2,489 | 5,996 | +141% | 0 | 0 | — |
case-09 | fail→fail | 9,657 | 8,281 | -14% | 1 | 1 | 0% | 1,908 | 4,987 | +161% | 0 | 0 | — |
case-10 | fail→pass | 12,168 | 7,260 | -40% | 1 | 1 | 0% | 2,342 | 4,834 | +106% | 0 | 0 | — |
case-11 | pass→pass | 7,888 | 4,065 | -48% | 1 | 1 | 0% | 1,271 | 4,244 | +234% | 0 | 0 | — |
case-12 | pass→pass | 8,740 | 4,696 | -46% | 1 | 1 | 0% | 1,709 | 4,311 | +152% | 0 | 0 | — |
case-13 | pass→fail | 10,819 | 7,709 | -29% | 1 | 1 | 0% | 2,024 | 5,044 | +149% | 0 | 0 | — |
case-14 | fail→fail | 7,622 | 4,930 | -35% | 1 | 1 | 0% | 1,439 | 4,381 | +204% | 0 | 0 | — |
case-15 | fail→pass | 13,117 | 5,699 | -57% | 1 | 1 | 0% | 2,690 | 4,562 | +70% | 0 | 0 | — |
case-16 | fail→pass | 12,161 | 4,800 | -61% | 1 | 1 | 0% | 2,039 | 4,296 | +111% | 0 | 0 | — |
case-22 | fail→pass | 9,916 | 10,093 | +2% | 1 | 1 | 0% | 2,115 | 5,510 | +161% | 0 | 0 | — |
case-17 | pass→pass | 10,333 | 4,902 | -53% | 1 | 1 | 0% | 1,954 | 4,413 | +126% | 0 | 0 | — |
case-18 | pass→pass | 3,681 | 4,677 | +27% | 1 | 1 | 0% | 671 | 4,071 | +507% | 0 | 0 | — |
case-19 | fail→fail | 10,207 | 7,047 | -31% | 1 | 1 | 0% | 1,980 | 4,917 | +148% | 0 | 0 | — |
case-20 | fail→pass | 14,324 | 14,733 | +3% | 1 | 1 | 0% | 2,507 | 5,789 | +131% | 0 | 0 | — |
case-21 | pass→pass | 9,741 | 10,109 | +4% | 1 | 1 | 0% | 1,832 | 5,607 | +206% | 0 | 0 | — |
case-23 | pass→pass | 13,077 | 12,149 | -7% | 1 | 1 | 0% | 2,258 | 5,975 | +165% | 0 | 0 | — |
case-24 | pass→pass | 11,980 | 12,802 | +7% | 1 | 1 | 0% | 2,517 | 6,047 | +140% | 0 | 0 | — |
case-25 | pass→pass | 8,868 | 10,490 | +18% | 1 | 1 | 0% | 1,849 | 5,628 | +204% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted. The headline lift of +36 percentage points is the difference between those two pass rates over the 25 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.