CypressでPiniaのストアのユニットテストを書く

[Cypress](https://docs.cypress.io/guides/overview/why-cypress)は`e2e`からUIコンポーネントまでの広い範囲を扱えるテスティングフレームワーク。

この記事では、[Pinia](https://pinia.vuejs.org/)のストアのユニットテストを`Cypress`を使って行う方法のアイデアを提示する。

## はじめに

`vue.js`のテンプレートを使用し、`Cypress`を組み込んでプロジェクトを作成した場合、以下の構成でテストが用意される。

* `./cypress/`フォルダ下に`e2e`のテスト
* `./src/components/__tests__/`フォルダ下にコンポーネントテスト

ここに、`./stores/下に配置したユニットテストを実行させたいが、

`Cypress`の`Testing Type-Specific Options`は`e2e`と`component`の設定しか用意されていなく、そのままではストアの設定を加えることはできない。

https://docs.cypress.io/guides/references/configuration#Testing-Type-Specific-Options

## 方法

まず、ストアのフォルダ構成は、以下のようになると想定している。

```
📁 /
├── src
    ├── components   
    ├── stores
        ├── 各ストアのフォルダ
            ├── index.ts   ここにストアの実装
            └── test.cy.ts ここにユニットテストの実装
```

ストアの近くにユニットテストを置きたかったのと、フォルダ分けせず`stores`直下にストアの実装とユニットテストの実装を置くと、フォルダが肥大化していや〜んな感じになるのが好みじゃないため。

こうしなければならないというわけではないです。

### 設定内容

`Cypress`のコマンドラインオプションとして、明示的に設定ファイルを渡すことができる。

そこで、`package.json`の`script`設定を以下のように記述。

```diff_json
-    "test:unit": "cypress run --component",
-    "test:unit:dev": "cypress open --component",
+    "test:c": "cypress run --component",
+    "test:c:dev": "cypress open --component",
+    "test:u": "cypress run --component --config-file cypress.stores.config.ts",
```

元々あった、コンポーネントテストを実行するためのコマンド`test:unit`を`test:c`に変更している。(`c`は`component`の`c`)

ユニットテスト用の設定ファイルは、以下のように記述した。

```ts
import { defineConfig } from 'cypress'

export default defineConfig({
  component: {
    specPattern: 'src/stores/**/*.{cy,spec}.{js,ts,jsx,tsx}',
    devServer: {
      framework: 'vue',
      bundler: 'vite'
    }
  }
})
```

ユニットテストなので`devServer`はなくてもいいのだけど、必須のためテンプレートでの記述をそのまま採用している。[^1]

[^1]: テスト実行時に裏でサーバが起動してしまうが、トレードオフとして受け入れることにした

### テストの記述

あとは、ストアのためのユニットテストを書く。
幸いにして、他の`BDD`フレームワークとだいたい同じ記法をサポートしてくれている。

たとえば、`vue`テンプレートでサンプルとして用意される`counter`ストアの場合は以下のようになる。

```ts
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '.'

describe('Counter store', () => {
    beforeEach(() => {
        setActivePinia(createPinia())
    })

    it ('increment', () => {
        const store = useCounterStore()
        expect(store.count).to.equal(0)
        store.count++
        expect(store.count).to.equal(1)
        store.count++
        expect(store.count).to.equal(2)
    })

    it ('double increment', () => {
        const store = useCounterStore()
        expect(store.doubleCount).to.equal(0)
        store.count++
        expect(store.doubleCount).to.equal(2)
        store.count++
        expect(store.doubleCount).to.equal(4)
    })
})
```

[Piniaの公式サイト](https://pinia.vuejs.org/cookbook/testing.html)で紹介された内容と比べると、以下の違い程度で済むのも魅力。

```diff_typescript
-    expect(counter.count).toBe(0)
+    expect(store.count).to.equal(0)
```

## おまけ

`Cypress`の一番気に入っている点が、`descrive` > `context` > `it`と階層化して書けること。
最近ははやりの`jest`とか`jest`とか`jest`は`context`が書けない（書けてもなんか不恰好）ことに不満だったのでよき。[^2]

[^2]: contextサポートしてるmocha好きだった。

## おまけその２

ユニットテスで、

```ts
import { useCounterStore } from '.'
```

と記述していて、おやっ？と思った人のための補足。

`vite.config.ts`のエイリアスの解決ルールとして以下のように記述しているため。

```diff_typescript
export default defineConfig({
  plugins: [
    vue(),
  ],
  resolve: {
    alias: {
+      '@stores/*': fileURLToPath(new URL('./src/stores/**/index.ts', import.meta.url)),
       '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})
```

コンポーネト側でのインポートで、以下のように手抜きをしたかったから。

```ts
import { useCounterStore } from '@/stores/counter'
```