diff --git a/README.md b/README.md index 1737db3..b61c87c 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ clasp login ``` 3. コマンド実行後に表示されたURLにアクセスしgoogleログインします。 4. ログイン後、リダイレクトされたURLをコピーします。 -5. 別のターミナルでコンテナの中に入り、curlコマンドでリダイレクトされたURLにアクセスします。 +5. 元のターミナルでコンテナの中に入り、curlコマンドでリダイレクトされたURLにアクセスします。 ```shell curl http://localhost:xxxx/?code=xxx ``` @@ -42,4 +42,21 @@ yarn build ### 8. Google App script に push する ``` yarn deploy -``` \ No newline at end of file +``` + +## テストの実行方法 + +### 1. テストを実行する +```shell +docker exec -it gemmini-ai-custom-function yarn test +``` + +### 2. テストカバレッジを確認する +```shell +docker exec -it gemmini-ai-custom-function yarn test:coverage +``` + +### 3. テストを監視モードで実行する +```shell +docker exec -it gemmini-ai-custom-function yarn test:watch +``` diff --git a/app/jest.config.js b/app/jest.config.js new file mode 100644 index 0000000..0103995 --- /dev/null +++ b/app/jest.config.js @@ -0,0 +1,24 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + transform: { + '^.+\\.tsx?$': ['ts-jest', { + useESM: true, + }], + }, + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + extensionsToTreatAsEsm: ['.ts'], + testMatch: ['**/__tests__/**/*.test.ts'], + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/**/__tests__/**', + ], + coverageDirectory: 'coverage', + globals: { + UrlFetchApp: {}, // モック用のグローバルオブジェクト + }, +}; diff --git a/app/package.json b/app/package.json index 5bab060..bfd4609 100644 --- a/app/package.json +++ b/app/package.json @@ -12,11 +12,14 @@ "lint:eslint": "eslint --ext \".js,.ts,.html\"", "fix:eslint": "npm run lint:eslint --fix", "prettier": "prettier --write .", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" }, "devDependencies": { "@google/generative-ai": "^0.2.1", "@types/google-apps-script": "^1.0.82", + "@types/jest": "^29.5.12", "@types/node": "^20.11.20", "@typescript-eslint/eslint-plugin": "^7.0.2", "@typescript-eslint/parser": "^7.0.2", @@ -32,11 +35,14 @@ "eslint-plugin-unused-imports": "^3.1.0", "gas-webpack-plugin": "^2.5.0", "html-webpack-plugin": "^5.6.0", + "jest": "^29.7.0", + "jest-environment-node": "^29.7.0", "prettier": "^3.2.5", "terser-webpack-plugin": "^5.3.10", + "ts-jest": "^29.1.2", "ts-loader": "^9.5.1", "typescript": "^5.3.3", "webpack": "^5.90.3", "webpack-cli": "^5.1.4" } -} +} \ No newline at end of file diff --git a/app/src/lib/__tests__/fetcher.test.ts b/app/src/lib/__tests__/fetcher.test.ts new file mode 100644 index 0000000..8ba50a3 --- /dev/null +++ b/app/src/lib/__tests__/fetcher.test.ts @@ -0,0 +1,82 @@ +import fetcher from '../fetcher'; + +// モックの型定義 +type MockResponse = { + getContentText: jest.Mock; +}; + +// グローバルモックの設定 +global.UrlFetchApp = { + fetch: jest.fn(), +} as unknown as typeof UrlFetchApp; + +describe('fetcher', () => { + // 各テスト前にモックをリセット + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('正常系: 正しいJSONレスポンスを返すこと', () => { + // モックレスポンスの準備 + const mockJsonResponse = { data: 'test data' }; + const mockResponse: MockResponse = { + getContentText: jest.fn().mockReturnValue(JSON.stringify(mockJsonResponse)), + }; + + // UrlFetchApp.fetchのモック実装 + (global.UrlFetchApp.fetch as jest.Mock).mockReturnValue(mockResponse); + + // テスト対象の関数を実行 + const url = 'https://example.com/api'; + const options = { method: 'GET' } as GoogleAppsScript.URL_Fetch.URLFetchRequestOptions; + const result = fetcher(url, options); + + // 検証 + expect(global.UrlFetchApp.fetch).toHaveBeenCalledWith(url, options); + expect(mockResponse.getContentText).toHaveBeenCalledWith('UTF-8'); + expect(result).toEqual(mockJsonResponse); + }); + + it('異常系: UrlFetchApp.fetchがエラーを投げた場合、エラーをラップして再スローすること', () => { + // UrlFetchApp.fetchのモック実装でエラーをスロー + const mockError = new Error('Network error'); + (global.UrlFetchApp.fetch as jest.Mock).mockImplementation(() => { + throw mockError; + }); + + // テスト対象の関数を実行し、エラーをキャッチ + const url = 'https://example.com/api'; + const options = { method: 'GET' } as GoogleAppsScript.URL_Fetch.URLFetchRequestOptions; + + // エラーがスローされることを検証 + expect(() => { + fetcher(url, options); + }).toThrow(`Error: ${mockError}`); + + // UrlFetchApp.fetchが呼ばれたことを検証 + expect(global.UrlFetchApp.fetch).toHaveBeenCalledWith(url, options); + }); + + it('異常系: JSONのパースに失敗した場合、エラーをラップして再スローすること', () => { + // モックレスポンスの準備(不正なJSON) + const mockResponse: MockResponse = { + getContentText: jest.fn().mockReturnValue('invalid json'), + }; + + // UrlFetchApp.fetchのモック実装 + (global.UrlFetchApp.fetch as jest.Mock).mockReturnValue(mockResponse); + + // テスト対象の関数を実行し、エラーをキャッチ + const url = 'https://example.com/api'; + const options = { method: 'GET' } as GoogleAppsScript.URL_Fetch.URLFetchRequestOptions; + + // エラーがスローされることを検証 + expect(() => { + fetcher(url, options); + }).toThrow('Error:'); + + // 検証 + expect(global.UrlFetchApp.fetch).toHaveBeenCalledWith(url, options); + expect(mockResponse.getContentText).toHaveBeenCalledWith('UTF-8'); + }); +});