@xstate/testはどう使うのか？

[![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16757/d2a3fbaf-30e1-f543-6350-c310c8b5982f.png)][1]


<iframe width="560" height="315" src="https://www.youtube.com/embed/pA3DXExjKqI?start=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


# 問題

[@xstate/test][1]のページをパッと見た限りだと「あーmachineがあって、それに沿ってテストするやつね？」って思うけど実は違う。

## 多分全ての人が初めにする勘違い

@xstate/testはxstateのmachineをテストする為（だけ）ではない。
テストする対象はxstateを使っていないただのページでも良いし、ただのJavascriptのコードでも良いし、@testing-library/reactでも良いし、puppeteerでもcypressでもいい。

### [createModel][2]

これは確かにxstateのmachineが必要だけど、**このmachineをテストしているわけじゃない（！！！）。**
パラメータとして渡しているこのmachineは**テスト対象ではなくて**、ユーザーとしてどういう動きをするだろうかの[paths][3]を作るためのmachine。
（xstateのmachineをテストしているのなら、得てして同じものになりがち、だがもう一度言うがこのmachineはテスト対象ではない）

> It's not "test code and implementation code" - it's test code and test code. The state machine describes the flows of the system under test, with assertions (in meta.test) that make sure each visited state is actually in their expected state.
[リンク][6]

> I've struggled to understand this too. It made more sense once I read this tweet. The goal is NOT to import your machine and generate tests from it. The goal is to write a NEW machine that acts like your users will, and test that. According to the xstate advice, you should not be trying to directly instrument your app's machine in the test files you create.
[リンク][9]
[リンク][7]

> Note that this finite state machine is used only for testing, and not in our actual application — this is an important principle of model-based testing, because it represents how the user expects the app to behave, and not its actual implementation details. The app doesn’t necessarily need to be created with finite state machines in mind (although it’s a very helpful practice).
[リンク][8]


### 結局何をテストしているのか？

[testPath.test(testContext)][4]ともあるように、ここに渡している`何か`(testContext)、をcreateModelに渡したmachineに記載している`meta: {test: {...}}`で検証している。


```tsx
const toggleMachine = createMachine({
...
...

  inactive: {
    on: {
      /* ... */
    },
    meta: {
      // path.test(page)に渡されたものが、まるっと全部最初のパラメータとして受け取れる
      // path.test([a, b, c])とすれば([a, b, c]) => {} として
      // path.test({a: 1, b: 2})とすれば({a, b}) => {} として
      test: async (page) => {
        await page.waitFor('input:checked');
      }
    }
  },

// 何度も言うが、このtoggleMachineはテスト対象ではない
const toggleModel = createModel(toggleMachine).withEvents({
...
})

...
...

  it(path.description, async () => {
    // 渡す値は自由
    // テストに必要なものは何でも突っ込んで、実際のテスト時に使える
    // ここではこのpageがテスト対象（puppeteerのページ）
    await path.test(page);
  });
```

### 前提条件 → アクション(操作) ​→ 事後条件 /（precondition → action → postcondition）

例えば、modalが開くボタンが1つだけあるページだとしても（**xstate有無不問**）

1. 初期表示（modal表示なし）
1. ボタンを押す（modal表示）
1. modal閉じるボタンを押す（modal非表示）

というユーザーとして起こすだろう操作がある。これをcreateModelに渡す`machine`として定義してやって、

- getShortestPathPlans
- getSimplePathPlans

から最終的にpathsを自動生成して、想定できる全てのルートを通るテストを行うということが@xstate/testの趣旨（だと今の所理解してる）。

> 実際はもっと複雑なprecondition → action → postconditionがあるはずで、[ダイアログが出る単純なページ][5]でさえもその複雑さが分かる


## createModelの使い方

1. テスト対象の何かがある（切り出したコンポーネントだったり、ページ自体だったり、Javascriptのクラスオブジェクトだったり）
1. 想定しうるユーザーとしてのprecondition → action（= event） → postconditionをmachineで表現する
1. このmachineのmeta -> testに実際のテストを書く
1. createModelにそのmachineを渡す
1. 加えてwithEventsに各アクション時にはどういう操作が必要かを書く
1. pathにテスト対象を渡す


# テストを実際にするにあたって

https://xstate.js.org/docs/guides/testing.html#testing-pure-logic

テスト対象がxstateのmachineだったとして...

## services

invokeするからには何らかの結果を期待しているわけだけど、多くの場合、他のサービスとのデータ連携だったりもする。
こういう場合はとてもテストがしづらいので、[service単体でテストは終えておいて][10]、テストをする際はmachineを使うコンポーネント、ページ単位で切り出しておいて、[該当のサービスは無効化するようなり処理][11]をして、単純にprecondition → action → postconditionがうまく画面の表示と合うかだけをテストするようにする。


```tsx:テスト対象の何か
// withConfigで渡すオブジェクトはコンポーネントのPropsにしておけばtest、production用と分けなくて良くなる
useMachine(machine.withConfig({
  services: {
    someService: () => { 
      // ここでうまいこと無効化する
    }
  }
}))
```

## actions

serviceと同様、単体でテスト + コンポーネントレベルなどのテストではうまいこと無効化する。

## guards

大抵はactionでcontextが変更された値を評価すると思うので、上の事が出来ていれば大丈夫じゃないかと。

## activities

これも多分同じ。


# meta: {test: { ... }}

```js
const toggleMachine = createMachine({
  id: 'toggle',
  initial: 'inactive',
  states: {
    inactive: {
      on: {
        /* ... */
      },
      meta: {
        test: async (page) => {
          await page.waitFor('input:checked');
        }
      }
    },
```

最初これを見た時は「おいおい、テストコードを成果物に埋め込むのか？」と思って、MachineConfigで切り出してみたりして実際のコードと、テスト用のコードと振り分けたりしてたけど、より複雑になっていく一方で「こんな使い方なわけがない...」と思って色々調べてたらやっぱり同じ様な事に悩んでる人がいて、よくよく読んでいくとやっぱり前提条件として`createModel(machine)`のmachineがテスト対象ではないと分かった。

[@xstate/test][1]のページ自体あっさりと書いてあるし、ネットでも@xstate/testを説明しているページがそんなに無いので、理解するまでに相当時間がかかった。


# リンク

https://www.youtube.com/watch?v=tkxH7o-n9sI&list=PLdWY3IGxchhdjUKJRVyeR_-LKX_QT6mcR

https://css-tricks.com/model-based-testing-in-react-with-state-machines/

https://dev.to/rjdestigter/model-based-ui-tests-with-xstate-cypress-puppeteer-more-268d

https://dev.to/rjdestigter/working-with-non-user-asynchronous-events-in-model-based-tests-with-xstate-58ek

https://spectrum.chat/statecharts/general/any-examples-of-auto-generated-tests-including-invoked-services~236ffd22-c5b7-4cfd-bde7-c0bfa8a5d06d

https://github.com/davidkpiano/xstate-test-demo

https://github.com/jtbales/model-based-testing-tutorial


[1]: https://xstate.js.org/docs/packages/xstate-test/
[2]: https://xstate.js.org/docs/packages/xstate-test/#createmodel-machine-options
[3]: https://xstate.js.org/docs/packages/xstate-test/#testplan-paths
[4]: https://xstate.js.org/docs/packages/xstate-test/#testpath-test-testcontext
[5]: https://youtu.be/pA3DXExjKqI?t=695
[6]: https://github.com/davidkpiano/xstate-test-demo/issues/8#issuecomment-723261857
[7]: https://twitter.com/iamsapegin/status/1181225125825449984
[8]: https://css-tricks.com/model-based-testing-in-react-with-state-machines/
[9]: https://github.com/davidkpiano/xstate-test-demo/issues/8#issuecomment-72491611
[10]: https://xstate.js.org/docs/guides/testing.html#testing-services
[11]: https://xstate.js.org/docs/guides/testing.html#mocking-effects