jestでテストをする

# Jestでのテストの基本構造

describeが大要素。その中にit, testがあり、一つのテストになっている。

describe, it, testの第一引数にはテストの説明を記述する。

第二引数には、ラムダ式でテストを記述していくのが基本形。

```tsx
// example: saveで保存した組織情報をgetで取得し、その結果と保存した組織情報が正しいか照合するテスト

import { saveOrganization } from "../db/organzation"

describe('Organizationの機能テスト', () => {
	
		 // 説明を記述。他の人が何をやっているかわかりやすいように  
    it('IdでOrganizationを取得', async () => {
        const savedOrg = await saveOrganization('Test Organization');
        const result = await getOrganizationById(savedOrg.id);
        expect(result).toEqual(savedOrg);
    });
});
```

# テストの前後で実行する関数の定義

describe()内で実行する。

dbへの接続、切断、データ構造の初期化などに使用する。

```tsx
// Describeの初期で実行
beforeAll();
// それぞれのit()の前で実行
beforeEach();
// それぞれのit()の後で実行
afterEach();
//  Describeの終わりで実行
afterAll(});
```

# テストをスキップする

テストが完全に実行されなくなる。

```tsx
describe.skip();
it.skip();
test.skip();

// it testに()をつける方が簡単
xit();
xtest();
```

# 同じテストケースを異なる引数で繰り返す

.each()で複数の要素を挿入する。

```tsx
describe.each(TABLE)(NAME, FN);
it.each(TABLE)(NAME, FN);
test.each(TABLE)(NAME, FN);

// exmaple
const multiply = (a, b) => a * b;
describe('multiply', () => {
  it.each([
    [1, 1, 1],
    [1, 2, 2],
    [2, 1, 2],
    [2, 2, 4],
  ])('multiply(%i, %i) should return %i', (a, b, expected) => {
    expect(multiply(a, b)).toBe(expected);
  });
});
```

# テストを評価する

Jestでは、`expect()`関数を使ってテストの評価を行う。`expect()`には、実際に実行して得られた値を引数として渡す。その後、マッチャーと呼ばれるチェーン関数を使用して、取得した値が想定の値と合っているかを確認する。

1. `toBe()`
    引数として渡された値と、`expect()`で渡された値が**完全に一致する**かを確認する。
    例: `expect(name).toBe('Test Client');`
2. `toEqual()`
    引数として渡されたオブジェクトや配列と、`expect()`で渡された値が同じ構造と値を持っているかを確認する。
     例: `expect(result).toEqual({ id: 1, name: 'John' });`
    
3. `toContain()`
    引数として渡された値が、`expect()`で渡された配列や文字列に含まれているかを確認する。
     例: `expect(result).toContain('apple');`
4. `toHaveProperty()`
    引数として渡されたプロパティ名が、`expect()`で渡されたオブジェクトに存在するかを確認する。
     例: `expect(result).toHaveProperty('id');`
5. `toHaveBeenCalled()` 
    `expect()`で渡されたモック関数が、テスト中に少なくとも1回は呼び出されたかを確認する。 
    例: `expect(mockFunction).toHaveBeenCalled();`
6. `toHaveBeenCalledWith()`
    `expect()`で渡されたモック関数が、テスト中に指定された引数で呼び出されたかを確認する。 
    例: `expect(mockFunction).toHaveBeenCalledWith(42);`

また、`not`修飾子を使用することで、逆の条件をチェックすることもできる。
例: `expect(result).not.toBe(null);`

### 応用的なマッチャーの紹介

`expect.objectContaining()`
要素の一部が合っていると、テストをPASSできる。
```tsx
// mockJsonの中身
//{
//  id: 10,
//  name: Tanaka,
//  prefecture: Gunma
//}

// 要素はidとnameしかないがパスできる
expect(mockJson).toHaveBeenCalledWith(expect.objectContaining(
	{
		id: 10,
		name: Tanaka
	}
));
```

`expect.arrayContaining()`
リストを扱う。複数要素があるときに、順番を気にすることなくマッチングできる。

`expect.any()`
プリンシパル型のみ判定する。
expect.any()は「ここに数字が入るが、自動で登録されるため具体的な数字がわからない」というときに便利

```tsx
// agentsの中身
//[
//   {
//.    id: 1
//     name: "Agent1",
//     llmModel: "gpt-3",
//     prompt: "prompt sample",
//     userGroupId: 10
//   },
//   {
//     id: 2
//     name: "Agent2",
//     llmModel: "gemini",
//     prompt: "prompt sample",
//     userGroupId: 20
//   },
// ]	

// 複数の要素を取得した場合でも、順番を気にすることなくマッチングが可能。
expect(agents).toEqual(expect.arrayContaining([
    expect.objectContaining({
      name: "Agent1",
      llmModel: "gpt-3",
      prompt: "prompt sample",
      userGroupId: expect.any(number)
    }),
    expect.objectContaining({
      name: "Agent2",
      llmModel: "gemini",
      prompt: "prompt sample",
      userGroupId: expect.any(number)
  ]));
```

# モック化する

モック化する目的は、テスト対象の関数や機能に依存する外部の関数やクラスの動作を制御し、テストを安定して実行できるようにするため。


### モック化の手順

1.  `jest.mock()`を使って、モック化対象の関数やクラスを指定する
    
    ```tsx
    // 外部モジュール
    jest.mock('jsonwebtoken');
    // 自作関数
    jest.mock('../db/repository/client', () => ({
      getOneClientByEmail: jest.fn(),
    }));
    ```
    
    jest.fn()でファンクションを作成。Mockオブジェクトになる。


2.  `mockResolvedValue()`や`mockRejectedValue()`を使って、モック関数の返り値を設定する
    
    getOneClientByEmail（自作関数）をモック化することで、mockCilentの値を返す
    
    ```tsx
    const mockClient = {
          id: '123',
          name: 'John Doe',
          email: 'test@example.com',
          password: await bcrypt.hash('password', 10),
          role: 'user',
          organization_id: 'org123',
        };
        
    // 正常動作の場合
    (getOneClientByEmail as jest.Mock).mockResolvedValue([mockClient]);
    
    // 異常動作の場合
    (getOneClientByEmail as jest.Mock).mockRejectedValue(new Error('Database connection error'));
    ```
    
    
3. テスト対象の関数を実行し、モック化された関数やクラスの動作を確認する
    
    ```tsx
    const req = mockRequest({ email: 'test@example.com', password: 'password' });
    await routClientLogin(req, mockResponse);
    ```
    

### モック化の例

1. 外部APIのモック化
    
    ```tsx
    jest.mock('axios');
    
    test('fetchUserData', async () => {
      const mockUser = { id: 1, name: 'John Doe' };
      (axios.get as jest.Mock).mockResolvedValue({ data: mockUser });
    
      const user = await fetchUserData(1);
      expect(user).toEqual(mockUser);
    });
    ```
2. クラスメソッドのモック化
    
    ```tsx
    jest.mock('./UserService');
    
    test('createUser', async () => {
      const mockUser = { id: 1, name: 'John Doe' };
      (UserService.prototype.create as jest.Mock).mockResolvedValue(mockUser);
    
      const userController = new UserController();
      const user = await userController.createUser('John Doe');
      expect(user).toEqual(mockUser);
    });
    ```
    

# **例外処理のテスト**

例外処理のテストでは、関数がエラーを投げる場合の動作を確認する。

## 同期関数のエラーテスト

同期関数がエラーを投げる場合、`toThrow` マッチャーを使ってテストを行います。

```tsx
function divide(a, b) {
  if (b === 0) {
    throw new Error('Division by zero');
  }
  return a / b;
}

it('0で除算した場合、エラーが投げられること', () => {
  expect(() => divide(10, 0)).toThrow('Division by zero');
});
```

上記の例では、`divide` 関数に0を渡した場合、'Division by zero'というエラーが投げられることを確認しています。`expect` の引数には、エラーを投げる関数を即時関数として渡します。

`toThrow` マッチャーには、期待されるエラーメッセージを文字列または正規表現で指定することができます。

```tsx
it('0で除算した場合、エラーが投げられること', () => {
  expect(() => divide(10, 0)).toThrow(/division by zero/i);
});
```

## 非同期関数のエラーテスト

非同期関数をテストする際、エラーが投げられる場合は `await` を使わずに `rejects` を使用する。これは、`await` を使用すると、エラーが発生した時点でテストが失敗してしまうため。



```tsx
it('無効な組織IDでロールを作成しようとした場合、エラーが投げられること', async () => {
  expect(createRole(RoleTypeEnums.Admin, 0)).rejects.toThrow(/^Error creating role.*/);
});
```

# **supertestを使ったAPIのテスト**

supertestを使うことで、Express.jsで作成したAPIのテストを簡単に行うことができる。ここでは、supertestを使ってルーティングを含むAPIのテストを行う方法について説明する。

## テストの基本構成

まず、supertestをインポートし、テスト対象のExpressアプリケーションをインポートする。

```tsx
import request from 'supertest';
import app from '../../api/v1/app';
...
```

次に、`describe`ブロック内で`it`を使ってテストケースを定義する。

```tsx
describe('GET /', () => {
  it('ルート("/")にGETリクエストを送信した場合、ステータスコード200が返され、レスポンスボディに"Running API"というメッセージが含まれていること', async () => {
    const res = await request(app).get('/');
		expect(res.status).toEqual(200);    
		expect(res.body).toHaveProperty('message', 'Running API');
  });
});
```

上記の例では、ルート('/')と'/api/v1'にGETリクエストを送信した場合のテストを行っている。`request(app)`でExpressアプリケーションにリクエストを送信し、`expect`を使ってレスポンスのステータスコードとボディの内容を確認している。

## Expressアプリケーションの設定

テスト対象のExpressアプリケーションでは、ルーティングの設定のみを行い、`listen`メソッドは呼び出さないようにする。もしくは、テストファイル内でルーティングの設定を行う。

```tsx
app.get("/", (req, res) => {
  res.status(200).json({ message: "Running API" });
});

app.use("/api/v1", v1Router);

export default app;
```

これは、Jestがテスト終了時にプロセスを自動的に終了させるため、`app`がリッスンし続けているとテストが終了できなくなるためである。

## 認証情報を追加する

認証が必要なAPIのテストを行う場合、`.set`メソッドを使ってAuthorizationヘッダーにトークンを設定する。

```tsx
const res = await request(app)
      .get(`${route}/clients`) 
      .set('Authorization', `Bearer ${token}`); // Authorization ヘッダーを設定

    expect(res.statusCode).toEqual(200);
    expect(res.body).toHaveProperty('message', 'Clients found');
   
```
この例では、`.set('Authorization', 'Bearer ${token}')`を使って、Authorizationヘッダーにベアラートークンを設定している。これにより、認証が必要なAPIのテストを行うことができる。

# 参考にした記事
https://qiita.com/YSasago/items/6109c5d3fbdbffa31c9f

https://qiita.com/yuta-katayama-23/items/29b91fed629fe758a42d



:::note info
弊社Passinate Geniusでは一緒に働く仲間を募集しています！興味をお持ちいただける方は、[ホームページ](https://passionategenius.com/)まで！
:::