Cloud Run functions × TypeScript 開発をしよう！【テスト：JestでTypeScriptの関数をテストする】

# はじめに
この記事では以下のことについて説明しています。
- JestでTypeScirptの関数をテストする方法
- Jest（ts-jest）のESM対応とモジュール解決方法

Jestの概要や書き方については説明しません。公式サイトを参考にしてください。

https://jestjs.io/ja/docs/getting-started

---

Jestは JavaScript のテスティングフレームワークです。TypeScriptのコードは Jest だけではテストできません。
以下のものが必要です。
- Jest
- ts-jest
- @types/jest

`ts-jest` によって TypeScript で書いたテストコードを直接テスト実行することができます。`@types/jest` は Jest のAPIの型定義ファイルです。

今回紹介するケースは ESM対応とモジュールインポートを実施したテストです。

# 必要なライブラリのインストール
```bash
npm install --save-dev jest ts-jest @types/jest
```

# `jest.config.json` の作成（TypeScript用）
```bash
npx ts-jest config:init
```

実行すると以下の `jest.config.json` が出力されます。

```js: jest.config.json
/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
  testEnvironment: "node",
  transform: {
    "^.+.tsx?$": ["ts-jest",{}],
  },
};
```

# テストの作成 / 実施
今回は以下のケースで説明していきます。
```bash: ディレクトリ構成
.
├── src
│   ├── hoge
│   │   └── hoge.ts
│   └── index.ts
├── test
│   └── hoge.test.ts
├── package.json
├── jest.config.js
└── tsconfig.json
```

`test` ディレクトリのコードも TypeScript のコンパイル対象に含めたいので、 `include` にその設定を追記しましょう。

https://www.typescriptlang.org/ja/tsconfig/#include

```js:tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "node16",
    "moduleResolution": "node16",
    "baseUrl": "./src",
    "paths": {
      "@/*": ["./*"]
    },
    "rootDirs": ["./src"],
    "outDir": "./dist",
    "sourceMap": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
  },
  "include": ["src/**/*", "test/**/*"], // "test/**/*" を追加する
  "exclude": ["dist", "node_modules"]
}
```

上記の `./src/hoge/hoge.ts` のテストを書いてみます。

```ts:./src/hoge/hoge.ts
export const hoge: () => string = () => "hogehoge";
```

```ts:./test/hoge.test.ts
test("check", () => {
  console.log("OK");
});
```

以下のコマンドを `pakcage.json` に追加します。

```js:package.json / testコマンド
  "scripts": {
    "test": "jest"
  },
```

```bash: test実行
$ npm run test

> advent-calendar-2024@1.0.0 test
> jest

  console.log
    OK

      at Object.<anonymous> (test/hoge.test.ts:2:13)

 PASS  test/hoge.test.ts
  ✓ check (14 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.62 s
Ran all test suites.
```

無事テストを実行することができました。

# ESM対応をする
例えば以下のようなモジュールインポートを行うテストを書いたとしましょう。
```ts: test/hoge.test.ts
import { hoge } from '@/hoge/hoge';

test("check", () => {
    const hogehoge = hoge();
    expect(hogehoge).toBe("hogehoge");
});  
```

これで `npm run test` を実行するとテストは成功しますが、`tsconfig.json` で `"module": "node16"` を指定しているためIDEのエラーチェックに引っかかります。
`import { hoge } from '@/hoge/hoge'` の部分で赤い波線と
```
Cannot find module '@/hoge/hoge' or its corresponding type declarations.ts(2307)
```
というエラーメッセージが表示されます。

![スクリーンショット 2024-12-11 16.23.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374872/379b1b18-6947-769a-b2c6-8fe9494b45df.png)


これは末尾に`.js`と拡張子を指定すると消えます。が、実行すると以下のようなエラーが出ます。

```bash: 失敗する
$ npm run test

> advent-calendar-2024@1.0.0 test
> jest

 FAIL  test/hoge.test.ts
  ● Test suite failed to run

    Configuration error:
    
    Could not locate module @/hoge/hoge.js mapped as:
    /Users/usr0302216/local/advent-calendar-2024/src/$1.
    
    Please check your configuration for these entries:
    {
      "moduleNameMapper": {
        "/^@\/(.*)$/": "/Users/xxx/local/advent-calendar-2024/src/$1"
      },
      "resolver": undefined
    }

    > 1 | import { hoge } from '@/hoge/hoge.js';
        | ^
      2 |
      3 | test("check", () => {
      4 |     const hogehoge = hoge();

      at createNoMappedModuleFoundError (node_modules/jest-resolve/build/resolver.js:759:17)
      at Object.<anonymous> (test/hoge.test.ts:1:1)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        1.094 s, estimated 2 s
Ran all test suites.
```
モジュールインポートに失敗しているようです。

ts-jest を使う上でESM対応の方法は
- ESM対応プラグインの導入
    - https://www.npmjs.com/package/ts-jest-resolver
- `node --experimental-vm-modules node_modules/jest/bin/jest.js` で実行する
    - https://jestjs.io/docs/ecmascript-modules
- `jest.config.js` にESM対応のコードを書く
    - https://kulshekhar.github.io/ts-jest/docs/guides/esm-support/

など様々な方法がありますが、1番目の「ESM対応プラグインの導入」を取ります。

### プラグインのインストール
```bash
npm install --save-dev ts-jest-resolver
```

### `jest.config.json` に `resolver: "ts-jest-resolver"` を追加
```js: jest.config.json / resolver
/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
  testEnvironment: "node",
  transform: {
    "^.+.tsx?$": ["ts-jest",{}],
  },
  resolver: "ts-jest-resolver",
};
```

しかし、これでも実行すると依然としてモジュール解決できずエラーが発生します。

```bash
$ npm run test

> advent-calendar-2024@1.0.0 test
> jest

 FAIL  test/hoge.test.ts
  ● Test suite failed to run

    Cannot find module '@/hoge/hoge.js' from 'test/hoge.test.ts'

    > 1 | import { hoge } from '@/hoge/hoge.js';
        | ^
      2 |
      3 | test("check", () => {
      4 |     const hogehoge = hoge();

      at Resolver._throwModNotFoundError (node_modules/jest-resolve/build/resolver.js:427:11)
      at Object.<anonymous> (test/hoge.test.ts:1:1)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        1.12 s, estimated 2 s
Ran all test suites.
```

実は Jest は何も設定していない状態で `tsconfig.json` の `paths` と `bashUrl` の設定を読み込むことができません。`jest.config.json` の `moduleNameMapper` オプションにその設定を伝える必要があります。

https://kulshekhar.github.io/ts-jest/docs/getting-started/paths-mapping

`jest.config.json` に以下を追記しましょう。

```js: jest.config.json / moduleNameMapper
/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
  testEnvironment: "node",
  transform: {
    "^.+.tsx?$": ["ts-jest",{}],
  },
  moduleNameMapper: {
    "^@/(.*)$": '<rootDir>/src/$1',
  },
  resolver: "ts-jest-resolver",
};
```

これで実行してみましょう。

```bash: 
$ npm run test

> advent-calendar-2024@1.0.0 test
> jest

 PASS  test/hoge.test.ts
  ✓ check (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.058 s
Ran all test suites.
```

これで無事テストを通すことができました。

---

以上、Jestを使ってTypeScriptの関数をテストする方法を紹介しました。
次回はもう一回くらいテストのことについて書いてみようと思います。

# おまけ
「言った通りにやったのに上手くいかないじゃん！！」という方もいらっしゃると思います。
一度テストディレクトリの名前を確認して見てください。 ディレクトリ名が間違っている可能性があります。`tests`（複数形）になっていますか？ `test`（単数系）になっていますか？

僕はこれで2時間くらい溶かしました。