JestとTesting-LibraryとMockについてまとめる

Jest・Testing-Library・Mockを使用してテストケースを記述した際に学んだことや注意点などをまとめる。

## JestとTesting-Libraryを用いたテスト
### テストケースの定義
`describe`でテストをグループ化し、`test`で実際のテストケースを定義する。
`expect`にテストしたいものを記述し、想定される結果をマッチャーを使用して記載する。
下記の例では、`toBe(true)`がマッチャーであり、想定される結果が`true`であることを記述している。
他にも`toContain`(配列に特定の要素が含まれている)など様々なマッチャーがある。
```js
describe('〇〇コンポーネント', () => {
  test('テストケース1', () => {
     expect(true).toBe(true);
  });
});
```

### コンポーネントのレンダリング
`render`で指定したコンポーネントをレンダリングできる。
レンダリング後に要素の取得などを行うことで、レンダリング結果に対するテストを記述できる。
`screen.debug`関数でレンダリングされたHTMLを確認できる。
```js
describe('App', () => {
  test('renders App component', () => {
    render(<App />);

    expect(screen.getByText('Search:')).toBeInTheDocument();
  });
});
```

`getByText`などの`getBy`から始まるものは指定された要素を取得し、なければエラーを返す。
`getAllByText`など`All`が付加されたものもあり、その場合は指定された要素をすべて取得する。
`queryBy`から始まるものは指定された要素が存在しない場合にエラーを返さないため、指定された要素が存在しないことを確認する場合に用いる。
`findBy`から始まるものは、非同期処理のものに用いる。対象が取得できる状態になるまで待機して取得してくれる。

### イベントの発火
`userEvent`を使用する。
```js
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
 
import App from './App';
 
describe('App', () => {
  test('renders App component', async () => {
    render(<App />);
 
    // wait for the user to resolve
    await screen.findByText(/Signed in as/);
 
    expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();
 
    await userEvent.type(screen.getByRole('textbox'), 'JavaScript');
 
    expect(
      screen.getByText(/Searches for JavaScript/)
    ).toBeInTheDocument();
  });
});
```

### 非同期
`async`と`await`を使用し、非同期の要素に対して`findBy`を使って記述する。
```js
import React from 'react';
import { render, screen } from '@testing-library/react';
 
import App from './App';
 
describe('App', () => {
  test('renders App component', async () => {
    render(<App />);
 
    expect(screen.queryByText(/Signed in as/)).toBeNull();
 
    screen.debug();
 
    expect(await screen.findByText(/Signed in as/)).toBeInTheDocument();
 
    screen.debug();
  });
});
```

## Mock
特定の関数を模倣することができる。
Mockを使用すると、下記のようなことが可能となり、テストが実装しやすくなる。
- 関数の呼び出し回数や引数を記録・参照
- 関数の戻り値の書き換え
- 関数の実装の書き換え

Mockの方法は大きく3パターンある。

### `jest.fn()`
mock関数を作成する。
mock関数は、呼び出しの回数や引数の内容を記録したり、実際の実装とは関係なく指定した戻り値を返すようにしたり、実際の実装を上書きしたりできる。
コールバック関数のテストなどに良い。

```js: jest.fn()
test('jest.fn()', () => {
const mockfn = jest.fn()

//返り値を指定。
//mockReturnValueOnceでは呼び出した順に返す返り値を指定できる。
//mockReturnValueOnceを使い切った場合は、mockReturnValueの値が使用される。
mockfn
  .mockReturnValue(0)
  .mockReturnValueOnce(1)
  .mockReturnValueOnce(2)

const firstReturnValue = mockfn(1);
const secondReturnValue = mockfn(2);
const thirdReturnValue = mockfn(3);

//呼び出しは3回。mockプロパティを参照することで引数や呼び出し回数が取得できる。
expect(mockfn.mock.calls).toHaveLength(3);

//2回目の呼び出しの第1引数は2
expect(mockfn.mock.calls[1][0]).toBe(2);

//返り値は順に1,2,0
expect(firstReturnValue).toBe(1)
expect(secondReturnValue).toBe(2)
expect(thirdReturnValue).toBe(0)

//mock関数の実装
mockfn.mockImplementation(() => { return 3 });

//実装により返り値として3が返る
expect(mockfn()).toBe(3)
})
```

### `jest.spyOn()`
オブジェクトのメソッドをmockできる。
元のメソッドの実装を変えずに監視したり、元のメソッドの実装を書き換えたりできる。
実装を書き換えた場合、テストを跨いで適用されてしまうので、`mockRestore()`で元の実装に戻すことができる。

```js: jest.spyOn()
  test('jest.spyOn()', () => {
    //オブジェクトのメソッドを定義
    const testObj = {
      fn: (i) => { return i }
    }

    //オブジェクトのメソッドをスパイする
    const spy = jest.spyOn(testObj, 'fn');

    //元のメソッドの実装は変わらない
    expect(testObj.fn(3)).toBe(3)

    //mock関数同様、呼び出し回数などを取得できる
    expect(spy.mock.calls).toHaveLength(1);

    //元のメソッドの実装を上書き。変更はテストをまたぐ。
    spy.mockImplementation((i) => { return i + 100 });
    expect(testObj.fn(3)).toBe(103)

    //元のメソッドの実装へ戻す
    spy.mockRestore();
    expect(testObj.fn(3)).toBe(3)
  })
```

### jest.mock()
モジュールをmockできる。
モジュールで`export`されている関数それぞれに対し、`jest.fn()`と同様の操作が可能。
**`jest.mock()`は`describe`や`test`の中に記述しても機能しないので注意。**

```js: Test.jsx
export const foo = () => {
  return 'foo'
}

export const bar = () => {
  return 'baz'
}
```

```js:Test.test.jsx
jest.mock('../Test.jsx')

test('jest.mock()', () => {
//fooの返り値を上書き
foo.mockReturnValue('foofoofoo');
expect(foo()).toBe('foofoofoo');

//mockプロパティにもアクセス可能
expect(foo.mock.calls).toHaveLength(1)

//barの実装を上書き
bar.mockImplementation(() => { return 'baz' })
expect(bar()).toBe('baz')

//barのメソッド確認用
console.log({ bar })
})
```
`console.log()`で`bar`の内容を確認した結果も記載する。
```js
{
  bar: [Function: bar] {
    _isMockFunction: true,
    getMockImplementation: [Function (anonymous)],
    mock: [Getter/Setter],
    mockClear: [Function (anonymous)],
    mockReset: [Function (anonymous)],
    mockRestore: [Function (anonymous)],
    mockReturnValueOnce: [Function (anonymous)],
    mockResolvedValueOnce: [Function (anonymous)],
    mockRejectedValueOnce: [Function (anonymous)],
    mockReturnValue: [Function (anonymous)],
    mockResolvedValue: [Function (anonymous)],
    mockRejectedValue: [Function (anonymous)],
    mockImplementationOnce: [Function (anonymous)],
    withImplementation: [Function: bound withImplementation],
    mockImplementation: [Function (anonymous)],
    mockReturnThis: [Function (anonymous)],
    mockName: [Function (anonymous)],
    getMockName: [Function (anonymous)]
  }
}
```

## その他注意点
- `getByText`は要素が複数あった場合、最初の要素を取得する
- `getByRole`は要素が複数あった場合、エラーになる
    - `{name: text}`で絞り込みできる
- `input type=number`に対して`userEvent.type()`が上手く適用できなかった
    - `fireEvent.change()`を使うと問題ない
- **`findBy`を使う際に`await`を忘れると正しく動作しないので注意**
- **`jest.mock()`は`describe`や`test`の中に記述しても機能しないので注意**
## 参考
https://jestjs.io/ja/docs/mock-functions

https://qiita.com/YSasago/items/6109c5d3fbdbffa31c9f

https://qiita.com/only/items/466a09c8602466fe2333

https://qiita.com/only/items/07634f917660f2e791b0