自作アプリにJest＋enzyme導入

🔰初心者です

前回導入編を紹介したので、更にdispatchした時やTextFieldに入力された時のバリデーションテスト等々を、自分用の備忘録がてら紹介していこうと思います。

（…まだまだエラーに躓きっ放しでございます！😭😭😭）


## ■ TextFieldに入力した文字が280文字を超えたらエラーを表示させる

今実装しているのが、「280文字を超えたらエラーを表示させる」テキストエリアです。下のように、Material-UIの`TextField`で用意されている`error`や`helperText`のプロパティを使ってエラーを表示させるようにしました。

```javascript
import TextField from '@material-ui/core/TextField'

class Form extends React.Component {
  state = {
    status: '',
    statusError: null,
  }
  handleChange = event => {
    const text = event.target.value
    this.setState({ status: text }, () => {
      this.validateStatus()
    })
  }
  validateStatus = () => {
    this.setState({
      statusError:
        this.state.status.length > 280
          ? '文字数は280文字以内に収めてください'
          : null,
    })
  }

  render() {
    return (
      <div>
        <TextField
          label="Let's Tweet!"
          placeholder="280文字以内で入力してください"
          margin="none"
          variant="filled"
          rows="5"
          multiline
          fullWidth
          onChange={this.handleChange}
          value={this.state.status}
          error={this.state.statusError ? true : false}
          helperText={
            this.state.statusError ? this.state.statusError : null
          }
        />
      </div>
    )
  }
}
```

`TextField`に`handleChange`を設置して、stateにある`status`に入れています。`this.setState()`の引数に280文字以上超えた時のバリデーションを用意しました。ユーザーがリアルタイムで入力してエラーが表示されるようにしています。

ちゃんと280文字超えたら`error`が`true`になってくれるか、テストしていきます。下はテストです。Jestとenzymeを使っています！
ちなみにこの

```javascript
import { storeFactory } from '@app/testUtils/testUtils'
```
は前回記事で書いたファイルです。→[記事](https://qiita.com/Hitomi_Nagano/items/df30da9086b705532aca)

```javascript
import * as React from 'react'
import { Provider } from 'react-redux'
import { storeFactory } from '@app/testUtils/testUtils'
import { createShallow, createMount } from '@material-ui/core/test-utils'
import Form from '@app/components/Form'
import TextField from '@material-ui/core/TextField'

const shallow = createShallow()
const mount = createMount()

const setup = (state = {}) => {
  const store = storeFactory(state)
  const wrapper = mount(
    <Provider store={store}>
      <Tweet />
    </Provider>
  )
  return wrapper
}

describe('<Tweet />', () => {
  it('Textareaが280文字の時、エラーメッセージは表示されない', () => {
    const wrapper = setup()
    wrapper
      .find(TextField)
      .find('textarea')
      .simulate('change', {
        target: {
          value:
            'あなたは翌日もしその紹介人というのの日にしましたい。はなはだ今朝をお話し人はとうとう同じ意見たありかもをしているんがは相違炙っございでて、一応には防ぐませたたまし。モーニングをするでしょのもちょうど将来を無論ないなます。いやしくも岡田さんへ開始権力まだ存在へ怒らです平気その責任皆か運動のというご意見ありましだでして、その同年はそれか文芸先生をできるば、嘉納さんののへ道の誰がもっと小ごろごろとさので私仕立をご合点の教えようにもうご始末をふらしますならから、はなはだいよいよ講演を買い占めるですからかかるたのに倒さなた。なおそれでお自我にするつもりもいっそ好き',
        },
      })

    expect(wrapper.find(TextField).props().error).toBe(false)
  })

  it('Textareaが281文字の時、エラーメッセージを表示する', () => {
    const wrapper = setup()
    wrapper
      .find(TextField)
      .find('textarea')
      .simulate('change', {
        target: {
          value:
            'あなたは翌日もしその紹介人というのの日にしましたい。はなはだ今朝をお話し人はとうとう同じ意見たありかもをしているんがは相違炙っございでて、一応には防ぐませたたまし。モーニングをするでしょのもちょうど将来を無論ないなます。いやしくも岡田さんへ開始権力まだ存在へ怒らです平気その責任皆か運動のというご意見ありましだでして、その同年はそれか文芸先生をできるば、嘉納さんののへ道の誰がもっと小ごろごろとさので私仕立をご合点の教えようにもうご始末をふらしますならから、はなはだいよいよ講演を買い占めるですからかかるたのに倒さなた。なおそれでお自我にするつもりもいっそ好きと',
        },
      })

    expect(wrapper.find(TextField).props().error).toBe(true)
  })
})
```

`wrapper`に`.find(TextField)`を繋げ、さらにその中にある`textarea`を探します。（今回は`TextField`に`multiline`を入れてテキストエリアにしています！テキストフォームの場合は`.find(input)`です。）
テキストエリアが入力されたイベントはenzymeで用意されている`simulate`を使っています。
最後にTextFieldの`error`プロパティは`wrapper.find(TextField).props().error`のように指定してあげます。この記述で正しく動作することを確認できました！

<img width="51" alt="2.png" src="https://qiita-image-store.s3.amazonaws.com/0/327745/d4ea26da-9aa5-e10c-859f-3d05f6db94e3.png">


▼ エラー解消してくれた記事

[Testing with Jest and Enzyme in React — Part 3 (Best Practices when testing with Jest and Enzyme)](https://medium.com/@wasuradananjith/testing-with-jest-and-enzyme-in-react-part-3-best-practices-when-testing-with-jest-and-enzyme-ae3fe0c39d06)

見ればわかるテストなので笑、Submitした時dispatchされるかテストしていきます！

##■ Submitした時、`dispatch`されることを確認する

上記でエラーメッセージが正しく表示されることを確認できました！
お次は、バリーデーションに引っかかっている状態の時はSubmitされては困るので、それもテストします。
- 0字の時はSubmitされない
- 1字以上280字以内の時、Submitされる
- 280字を超えたらSubmitされない

今回は、Submitされた時`this.props.postTweetStatus`が動作するように書いています。

```javascript
const setup = (state = {}) => {
  const props = {
    postTweetStatus: jest.fn()
  }
  const store = storeFactory(state)
  const wrapper = mount(
    <Provider store={store}>
      <Tweet {...props} />
    </Provider>
  )
  return { wrapper, props }
}

describe('dispatchの動作テスト', () => {
  it('0文字の時dispatchされない', () => {
    const { wrapper, props } = setup()
    const submitButton = wrapper
      .find(`[data-test="submitButton"]`)
      .at(0)
      .find('button')
    expect(props.postTweetStatus).not.toHaveBeenCalled()
  })

  it('0字以上280文字以内の時正しくdispatchされる', () => {
    const { wrapper, props } = setup()
    const submitButton = wrapper
      .find(`[data-test="submitButton"]`)
      .at(0)
      .find('button')
    wrapper.setState({ status: 'test' })

    submitButton.simulate('click', () => {
      const { wrapper, props } = setup()
      const submitButton = wrapper
        .find(`[data-test="submitButton"]`)
        .at(0)
        .find('button')
      expect(props.postTweetStatus).toHaveBeenCalled()
    })
  })

  it('281字以上の時dispatchされない', () => {
    const { wrapper, props } = setup()
    const submitButton = wrapper
      .find(`[data-test="submitButton"]`)
      .at(0)
      .find('button')
    wrapper.setState({ status: 'あなたは翌日もしその紹介人というのの日にしましたい。はなはだ今朝をお話し人はとうとう同じ意見たありかもをしているんがは相違炙っございでて、一応には防ぐませたたまし。モーニングをするでしょのもちょうど将来を無論ないなます。いやしくも岡田さんへ開始権力まだ存在へ怒らです平気その責任皆か運動のというご意見ありましだでして、その同年はそれか文芸先生をできるば、嘉納さんののへ道の誰がもっと小ごろごろとさので私仕立をご合点の教えようにもうご始末をふらしますならから、はなはだいよいよ講演を買い占めるですからかかるたのに倒さなた。なおそれでお自我にするつもりもいっそ好きと' })

    submitButton.simulate('click', () => {
      expect(props.postTweetStatus).not.toHaveBeenCalled()
    })
  })
})
```

`setup()`のところにActionで書いている関数を`jest.fn()`で設定してあげます。
それをwrapperのコンポーネントタグで`{...props}`のように渡してあげて前準備は完成です！

```javascript
const props = {
  postTweetStatus: jest.fn()
}
```

`wrapper.setState({ ... })`でwrapperのローカルステートを更新しています。
ダミーテキストは適当に入れちゃいましたがもっと細かくできるかも（？👀）
そのあとに`simulate`でクリックさせた上で予想している動作を書いてあげます。

```javascript
submitButton.simulate('click', () => {
  // テスト
})
```

参考にしたYoutube → [Mocking Axios in Jest + Testing Async Functions](https://www.youtube.com/watch?v=9Yrd4aZkse8)

テスト結果はオールパス！！😭🎉
ここに行き着くまでもかなり時間掛かっています…！

![3.png](https://qiita-image-store.s3.amazonaws.com/0/327745/4950b13e-0f0a-dc45-f85e-ebf403054352.png)

##■ 記事を取得後、記事のlength分ループして記事が表示されている

今までstoreを使う必要があったので`mount`で対応していましたが、このテストでは上手くいきませんでした…
mountでテストしようとすると、読み込んでいる子コンポーネントの値が取れないとエラーが表示されてしまうのです…！😭💦 なので今回は`shallow`の方を使い、逆にコンポーネント側をテストに合わせて少しだけ作り変えています。

記事一覧で実装したテストは3点です。
- 新規登録した時や取得する記事が０件の時、デフォルト表示用に用意したdivタグの表示を確認
- ローディング時はローディングだけのdivが表示
- 記事取得後、記事のlengthと表示のループ回数が一緒

詳細を省きますが、コンポーネントの方はこのように実装しました。

#####ローディング
propsで渡ってくる`timeline.loading`がtrueの時、`<CircularProgress />`を表示させています。タグにはテスト用のタグ、`data-test="loadingView"`を付与しました。

#####記事が0件の時（例えば初回ログイン時の時など）
`props.timeline.homeTimeline.length`の記事がイコール0の時、`data-test="firstSignUpView"`を付与してあるdivタグを表示させます。
#####エラー
`timeline.error`がtrueの時、`data-test="errorView"`を付与したdivを表示させます。
#####記事取得成功
`data-test="timlineView"`を付与したdivを表示するようにしています。

```javascript
export class TimelineLists extends React.Component {
  render() {
    const { timeline } = this.props

    if (timeline.loading) {
      return <CircularProgress data-test="loadingView" />
    } else if (timeline.homeTimeline.length === 0) {
      return (
        <div data-test="firstSignUpView">
          <p>早速記事を投稿してみましょう！</p>
          <Button>記事を投稿する</Button>
        </div>
      )
    } else if (timeline.error) {
      return (
        <div data-test="errorView">
          取得に失敗しました
          <Button>再取得する</Button>
        </div>
      )
    }
    return (
      <div data-test="timlineView">
        {timeline.homeTimeline.map(article => {
          return (
            <div key={article.id} data-test="articleView">
              <TimelineArticleMedia article={article} />
            </div>
          )
        })}
      </div>
    )
  }
}
export default TimelineLists
```

結構省いてしまいましたが、Reduxを使っているので`connect`も指定しているし、ページ遷移用のボタンもあったので、`react-router-dom`も読み込ませています。

テストがしやすいように、classの前に`export`を付けました。

```javascript
export class TimelineLists extends React.Component {
```

実際のテストがこちらです。


```javascript
import * as React from 'react'
import { createShallow, createMount } from '@material-ui/core/test-utils'
import { TimelineLists } from '@app/components/TimelineLists'

const shallow = createShallow()
const mount = createMount()

const initialProps = {
  timeline: {
    homeTimeline: [],
    loading: false,
    error: null
  },
}

const setup = (props = initialProps) => {
  const wrapper = shallow(<TimelineLists {...props} />)
  return wrapper
}

describe('<TimelineLists />', () => {
  describe('各ステータス表示テスト', () => {
    it('新規登録時または記事0件の時、"firstSignUpView"が表示される', () => {
      const wrapper = setup()
      expect(wrapper.find(`[data-test="firstSignUpView"]`).exists()).toBe(true)
    })

    it('ローディング時"loadingViewが表示される', () => {
      const props = {
        timeline: {
          homeTimeline: [{ id: 1 }, { id: 2 }, { id: 3 }],
          loading: true,
          error: null,
          initialTimelineLoad: false
        },
      }
      const wrapper = setup({}, props)

      expect(wrapper.find(`[data-test="loadingView"]`).exists()).toBe(true)
    })

    it('記事一覧取得成功時、記事のlengthと同じループ処理がされる', () => {
      const props = {
        timeline: {
          homeTimeline: [{ id: 1 }, { id: 2 }, { id: 3 }],
          loading: false,
          error: null,
          initialTimelineLoad: false,
        },
      }
      const wrapper = setup({}, props)

      expect(wrapper.find(`[data-test="articleView"]`)).toHaveLength(
        props.timeline.homeTimeline.length
      )
    })
  })
})
```

テストしたい状態を`const props = {}`の中で再現し、`setup()`関数の引数に渡しています。
`it(() => {})`それぞれのpropsを変えてテストしています。
今回は存在の確認だったので、enzymeが用意している`exists()`のtrue / falseで確認しています！

<img width="515" alt="4.png" src="https://qiita-image-store.s3.amazonaws.com/0/327745/93491ff1-7d55-2157-b7c7-f138af2ef80f.png">


長くなったので、今回はここで締めることにします。次回もテストについて！💻

【やってみて】
絶っっ対他に方法がある…と確信しています。
冗長的だし、より複雑なテストをするとなると今のコードでは太刀打ち出来ないかも👀
もっと深掘りしていきます。


【終わりに】
今携わっている案件で、バグの対応を依頼されました。
エラーを見ると共通ヘッダーコンポーネントでエラーが出ている様子…
ほぼ全コンポーネントでヘッダー読み込ませているので、例えば１箇所変更してもどこがどう変わっちゃうのか分かりません…！
スナップショットテストを導入したら、その話をQiitaに載せようと思います。

話は脱線しますが、このQiita記事を書いている間に、VSCodeのテーマを変更しました♫
今のテーマは「[Dracula Official](https://marketplace.visualstudio.com/items?itemName=dracula-theme.theme-dracula)」！！！🧛‍♂️
ピンクが可愛い！！！！

（こちらの記事を参考にVSCodeのプラグインを入れたり消したりしました。[The Ultimate VSCode Setup for Front End/JS/React](https://medium.com/productivity-freak/the-ultimate-vscode-setup-for-js-react-6a4f7bd51a2)）

エラー画面を見ても盛り下がらない工夫必要w