こんにちは。Photoraitエンジニアのヒエイです。

PhotoraitではWebつくというWebサイト作成ツールを提供しており、Reactを使用しています。

フロントエンドテストを導入した話を書きます。

前提

Photorait Webつく技術スタック

React 16.13
TypeScript 3.9

Jest導入

方針

  • JestEnzymeを利用
    • Jest
      facebook社製のテスティングフレームワーク。
    • Enzyme
      Reactテストライブラリ・ユーティリティツール。

パッケージインストール

npm install --save-dev jest @types/jest ts-jest enzyme enzyme-adapter-react-16 @types/enzyme @types/enzyme-adapter-react-16

初期設定

  • React・Typescriptで実行のための初期設定

tsconfig.jest.json

  • Jest実行用に別で用意。アプリケーション用tsconfigを上書きさせる形にしました。以下はサンプルです。
{
    "extends": "./tsconfig.json",
    "compilerOptions": {
        "moduleResolution": "node",
        "allowJs": true,
        "noEmit": true,
        "strict": true,
        "jsx": "react",
        "baseUrl": ".",
        "paths": {
            "~/*": ["ソースのあるディレクトリ/*"],
        },
        "lib": ["ES2019", "dom"],
        "module": "commonjs",
        "target": "ES2019",
        "resolveJsonModule": true
    },
    "exclude": [
        "node_modules"
    ]
}

 

jest.config.js

  • Jest用設定ファイルを用意
const { pathsToModuleNameMapper } = require('ts-jest/utils');
const { readFileSync } = require('fs');
const { parse } = require('jsonc-parser');
const { compilerOptions } = parse(readFileSync('./tsconfig.jest.json').toString());
const moduleNameMapper = pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/' });


module.exports = {
  preset: 'ts-jest',
  globals: {
    'ts-jest': {
      tsconfig: 'tsconfig.jest.json',
      diagnostics: false
    }
  },
  roots: ["<rootDir>/ソースのあるパス"],
  transform: {
    "^.+\\.(ts|tsx)$": "ts-jest",
  },
  transformIgnorePatterns: [
    "/node_modules/*"
  ],
  testRegex: "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
  testPathIgnorePatterns: [
    "/node_modules/"
  ],
  moduleNameMapper,
  moduleFileExtensions: [
    "ts",
    "tsx",
    "js",
    "json"
  ]

実行

  • package.json を修正
"scripts": {
    ・・・
    "test": "jest"
  },
・・・

実行は以下

npm run test

テストコード設置

テスト対象の簡単な入力フォームを用意しました。

import React, { FormEvent, useState } from 'react'
import { Button, Form, Header, Input } from 'semantic-ui-react'

type Props = {
  text: string
  postRequest: (formValue: string) => void
}

const FormComponent: React.FC<Props> = ({ text, postRequest }) => {
  const [formValue, setFormValue] = useState(text)

  const handleChange = (changeValue: string): void => {
    setFormValue(changeValue)
  }

  const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
    e.preventDefault()
    postRequest(formValue)
  }

  return (
    <>
      <Header as="h2">サンプルフォーム</Header>
      <Form onSubmit={(e): void => handleSubmit(e)}>
        <label htmlFor="sample_text">テキスト</label>
        <Form.Field width={8}>
          <Input
            id="sample_text"
            type="text"
            name="sample_text"
            placeholder="テキストを入力"
            value={formValue}
            onChange={(e): void => handleChange(e.target.value)}
          />
        </Form.Field>

        <Button>
          送信
        </Button>
      </Form>
    </>
  )
}

export default FormComponent

テキストボックスに入力した内容をform submitで送るコンポーネントです。

postRequest には送信メソッドを設定します。

 

jestテストコードを簡単には以下の形で用意します。

import React from 'react'
import { mount, configure } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'

import FormComponent from '~/components/form'

configure({ adapter: new Adapter() })
const formPostRequest = jest.fn()

describe('sample form test', () => {
  test('form submit test', () => {
    const inputText = 'テキストテキストテキスト'
    const wrapper = mount(
      <FormComponent
        text=""
        postRequest={formPostRequest}
      />
    )
    wrapper
      .find('input')
      .find({ name: 'sample_text' })
      .simulate('change', {
        target: {
          value: inputText
        }
      })
    wrapper.find('form').simulate('submit')
    expect(formPostRequest).toHaveBeenCalledWith(inputText)
  })
})

 

解説

enzyme読み込み

import React from 'react'
import { mount, configure } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'

configure({ adapter: new Adapter() })

React 16 を利用する際の読み込みはこちらを参考

 

mock化

const formPostRequest = jest.fn()

アタッチするpostリクエスト用関数をjestのmock関数で用意します。

 

Jestテスト

const wrapper = mount(
      <FormCompornent
        text=""
        postRequest={formPostRequest}
      />
)

フォームコンポーネントを展開します。

 

const inputText = 'テキストテキストテキスト'
wrapper
    .find('input')
    .find({ name: 'sample_text' })
    .simulate('change', {
      target: {
        value: inputText
      }
    })

inputテキストボックスに文字列を入力しています。

 

wrapper.find('form').simulate('submit')
expect(formPostRequest).toHaveBeenCalledWith(inputText)

フォームをサブミットさせ、アタッチしたformPostRequest関数に入力した文字列通り渡ってくるかテストしています。

 

懸念点・要調査

    • React Hooksの状態をEnzyme側で取得ができず、状態テストを上手く行えないようでした。
    • 状態変更後のエレメントの数の変化等でテストを入れる事にしています。

まとめ

簡潔にも汎用的にもテストを仕込めるのですごく便利でした。

次回はstorybook とスナップショットテスト(storyshot)について触れたいと思います。

 

 

Join Us !

ウエディングパークでは、一緒に働く仲間を募集しています!
ご興味ある方は、お気軽にお問合せください(カジュアル面談から可)

採用情報を見る