Jest + TypeScript + React Native で、 react-native モジュールを spy する

# 概要

Jest は、ルートディレクトリに `__mocks__/react-native.js` などといったファイルを置いておくと、自動的にモックしてくれる。

が、モックされたオブジェクトがちゃんと呼ばれたかを調べたかった。今まで Jest のモック周りを曖昧にしていたが、これを機にできるようにしたかった。

React Native は、 ネイティブの API をモックすることが多いので題材にした。
このやり方で、どのプロジェクトでも Jest であれば対応できると思う。

# やること

`import { Share } from 'react-native'` この `Share.share` をモックしたい。

あるクラス `MyClass` があって、これが `share` することを確かめたい。

# 前提

```
    "react-native": "0.57.7",
    "typescript": "^3.2.2",
    "react-native-typescript-transformer": "^1.2.10",
```

設定ファイルはこんな感じ。

```js:jest.config.js
module.exports = {
  preset: 'react-native',
  moduleFileExtensions: ['ts', 'tsx', 'js'],
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
    '^.+\\.(js)$': '<rootDir>/node_modules/react-native/jest/preprocessor.js',
  },
  testRegex: '(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$',
  testPathIgnorePatterns: ['<rootDir>/dist', '\\.snap$', '<rootDir>/node_modules/', 'mock-server'],
  cacheDirectory: '.jest/cache',
  globals: {
    'ts-jest': {
      tsConfig: 'tsconfig.jest.json',
      diagnostics: {
        warnOnly: true,
      },
    },
  },
}
```

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "declaration": false,
    "sourceMap": true,
    "removeComments": true,
    "noLib": false,
    "skipLibCheck": true,
    "jsx": "react-native",
    "types": ["node", "jest"],
    "typeRoots": ["typings"],
    "lib": ["dom", "es2015", "es2016", "es2017", "esnext.asynciterable"],
    "keyofStringsOnly": true,

    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,

    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,

    "allowSyntheticDefaultImports": false
  }
}
```

# 実装

まずは、テストされるクラス。

```ts:src/MyStore.ts
import { Share } from 'react-native'

export class MyStore {
  share(title: string, url: string) {
    return Share.share({
      title,
      url,
    })
  }
}
```

次に、テストコード。

```ts:src/MyStore.test.ts
import { MyStore } from './MyStore'
import { Share } from 'react-native'

test('MyStore can share', async () => {
  // Create store instance
  const myStore = new MyStore()

  // The point is here!
  // `jest.spyOn` takes two arguments:
  //     1. object to be mocked
  //     2. method to be called
  //
  // And we define the mock implementation.
  jest
    .spyOn(Share, 'share')
    .mockImplementation(jest.fn(() => 'Shared!'))

  const res = await myStore.share('Hello', 'https://example.com')

  console.log(res) // => 'Shared!'

  // We can check mock implementation is called here.
  expect(Share.share).toBeCalledWith({
    title: 'Hello',
    url: 'https://example.com',
  })
})
```

# 考察

実際のアプリケーションでは、毎回モックするのは大変だし、やる必要が無いので、 `__mocks__` 以下に適当なモックは置いておいて、適宜 `jest.spyOn` するのがバランス良いだろう。

また、 Jest のはまりどころとして、 モックはファイル毎にしか定義できない（= `test` の内部でモックは書けない）ので、複数のパターンを試したい場合は

- テストケース毎にファイルを分割する
- モック部分で、入力や呼ばれた回数に応じて、出し分ける

などする。

# 参考

- https://jestjs.io/docs/en/mock-functions
- https://lealog.hateblo.jp/entry/2017/11/08/122334

# 追記

書いてから、すぐに追記するスタイル。

下記のモックファイルでも、ほぼ同様のことができる。

```js:__mocks__/react-native.js
module.exports = {
  Share: {
    share: jest.fn(() => 'Shared!'),
  },
}
```

使い分けとしては、

- 「外部APIをこのように呼ぶ」ことを確認したい時は `__mocks__` （手軽なので）
- 「外部APIの挙動に応じて」動作を確認したい時は `spyOn`


で、良いだろう。