Jestを利用して、Stripe Appsのアプリをテストする方法



この記事は、[Stripe Apps を25日間紹介し続ける Advent Calendar 2022](https://qiita.com/advent-calendar/2022/stripe-apps) 13日目の記事です。

![スクリーンショット 2022-11-24 17.56.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/e0261865-0418-daa2-423a-5f4d70558b1b.png)

アプリを長期的に運用保守する上で、ユニットテストの導入は欠かせません。

Stripe AppsはReact / TypeScriptをベースに開発できますが、テストについてもReactと同様Jestで実行できます。

## Stripe Apps アプリで、Jestのテストを実行する

Stripe CLIでセットアップした際に生成されるテストファイルをみてみましょう。

```tsx
import {render, getMockContextProps} from "@stripe/ui-extension-sdk/testing";
import {ContextView} from "@stripe/ui-extension-sdk/ui";

import App from "./App";

describe("App", () => {
  it("renders ContextView", () => {
    const {wrapper} = render(<App {...getMockContextProps()} />);

    expect(wrapper.find(ContextView)).toContainText("save to reload this view");
  });
});
```

JestでReactのテストを書いたことのある方は、かなり見慣れたコードに見えるかもしれません。

通常のテストと異なる点として、`render`などの関数を`@stripe/ui-extension-sdk`からimportしています。

### `npm run test`でテストを実行する

テストは、`npm run test`または`npx jest`などで実行できます。

```bash
$ npm run test
jest
 PASS  src/views/App.test.tsx
  Setting
    ✓ renders AppView (6 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.838 s
```

### TypeScriptの型チェックにも対応

Jestのユニットテストは、TypeScriptの型チェックもサポートしています。

試しにTypeScriptの設定を変更して、テストしてみましょう。

`tsconfig.json`を次のように変更します。

```diff_json
{
  "extends": "@stripe/ui-extension-tools/tsconfig.ui-extension",
  "compilerOptions": {
    "noUnusedParameters": true
  }
}

```

`noUnusedParameters`を`true`にして、「使用していないパラメタ」が残っているとエラーが出るようにしました。

この状態でテストを再度実行しましょう。

```bash
$ npm run test
jest
 FAIL  src/views/App.test.tsx
  ● Test suite failed to run

    src/views/App.tsx:11:18 - error TS6198: All destructured elements are unused.

    11 const App = ({userContext, environment}: ExtensionContextValue) => {
                        ~~~~~~~~~~~~~~~~~~~~~~~~~~

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        2.866 s, estimated 3 s
error Command failed with exit code 1.
```

エラーが発生しました。

このように、コード内に型エラーが存在しているかの確認もJestで行えます。

## Stripe AppsアプリのUIテストを書く

Stripe Apps用アプリのテストコードは、ほぼReactでのテストと同様に実装できます。

Apps用のメソッドがいくつか追加されていますので、ドキュメントも参考にしましょう。

https://stripe.com/docs/stripe-apps/ui-testing

### `render`で描画して、`find`で探して`toXXX`でテストする

大まかにはこの３ステップでテストコードを実装します。

次のコードでは、「`App`内にある、`href="http://example.com"`プロパティを持つ`Button`要素のテキストが`Press me`を含む」ことをテストしています。

```typescript
  const {wrapper} = render(<App />);
  const button = wrapper.find(Button, {href: 'http://example.com'});
  expect(button).toContainText('Press me');
```

より細かい条件で検索するために、`findWhere`を利用することもできます。

```typescript
  const button = wrapper.findWhere<typeof Button>(
    (node) => node.is(Button) && node.prop('href').startsWith('http://example'),
  );
```

### スナップショットテストも可能

レンダリングした内容が変わっていないかを確認できる、「スナップショットテスト」も利用できます。

```typescript
import {render, getMockContextProps} from "@stripe/ui-extension-sdk/testing";

import App from "./App";

describe("App", () => {
  it("Snapshot test", async () => {
    const {wrapper} = render(<App {...getMockContextProps()} />);
    expect(wrapper.find(ContextView)).toMatchSnapshot();
  });
});
```

このテストコードを実行すると、初回はスナップショットファイルが生成されます。

```bash
$ jest src/views/App.t
 PASS  src/views/App.test.tsx
  App
    ✓ Snapshot test (6 ms)

 › 1 snapshot written.
Snapshot Summary
 › 1 snapshot written from 1 test suite.
```

この後、`src/views/App.tsx`の中身を変更すると、次のテストでエラーが発生します。

```diff
    @@ -557,11 +557,11 @@
        "is": [Function],
        "prop": [Function],
        "props": Object {
          "externalLink": Object {
            "href": "https://stripe.com/docs/stripe-apps",
-           "label": "View docs",
+           "label": "View Docs",
          },
          "title": "Hello",
        },
        "text": "cus_1234",
        "toString": [Function],
```

`View docs`の`D`の大文字小文字が変わっていることを検知できました。

変更が意図したものの場合、`-u`をつけて再実行することでスナップショットを更新できます。

```bash
$ jest src/views/App.tsx -u
 PASS  src/views/App.test.tsx
  App
    ✓ Snapshot test (8 ms)

 › 1 snapshot updated.
Snapshot Summary
 › 1 snapshot updated from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 updated, 1 total
Time:        1.188 s, estimated 3 s
```

:::note warn
**`render`後、`find`で`XXXView`を取得しよう**

`render`した戻り値の`wrapper`をそのまま`toMatchSnapshot`にかけると、意図しないスナップショットが生成されます。

```
exports[`Snapshot test 1`] = `
Object {
  "act": [Function],
  "unmount": [Function],
}
`;
```

`find`などで取得したものに対して、テストを実行しましょう。
:::

### Viewのエントリーコンポーネントには、`getMockContextProps`

Next.jsなどと同様。Viewごとにエントリーコンポーネントを作ります。

このため、`userContext`や`environment`など、アプリから渡される値をテストコードでも再現する必要があります。

TypeScriptの型情報を元に作成することもできますが、`getMockContextProps()`を利用するとより簡単になります。

```typescript
import {render, getMockContextProps} from "@stripe/ui-extension-sdk/testing";
import {ContextView} from "@stripe/ui-extension-sdk/ui";

import App from "./PaymentListView";

describe("App", () => {
  it("renders ContextView", async () => {
    const {wrapper} = render(<App {...getMockContextProps()} />);
```

`getMockContextProps()`は、引数で上書きしたい値を指定できます。

例えば、顧客ページ(`stripe.dashboard.customer.detail`)のテストを行いたい場合、`environment`の値を`Customer`データに変更できます。

```diff_typescript
-    const {wrapper} = render(<App {...getMockContextProps()} />);
+    const {wrapper} = render(<App {...getMockContextProps({
+      environment: {
+        objectContext: {
+          id: 'cus_1234',
+          object: 'customer'
+        }
+      }
+    })} />);
```

`environment.objectContext.object`で、オブジェクトの種類を判別する処理を書いている場合などに利用しましょう。


## Stripe Appsひとりアドベントカレンダー 2022

今年ベータリリースされたばかりのStripe Appsは、まだ日本語の情報が多くありません。

そこでQiita Advent Calendar 2022にて、毎日Stripe Appsについての情報を投稿します。

https://qiita.com/advent-calendar/2022/stripe-apps

ノーコードで利用する方法や、開発するためのTipsなども紹介予定ですので、ぜひ購読をお願いします。