0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

CRA + Redux Toolkit の初期テストコード

Last updated at Posted at 2021-07-04

概要

Redux Toolkitを勉強しています。
CRA(Create React App)でプロジェクトを作成して、初期テストコードがちょっとしか無かったので、RTL(React Testing Library)による基本的なテストを書いてみました。
細かいとこを詰めてないのですが、ご参考ください。

もっと適切な書き方などあれば、教えてもらえると嬉しいです。

環境

  • @testing-library/jest-dom : ^5.14.1
  • @testing-library/react : ^12.0.0
  • react-scripts : 4.0.3
  • typescript : ^4.1.5

始め方

  • CRAでプロジェクト作成
    • テンプレートはtypescript, redux-typescript
yarn create react-app my-app --template redux-typescript
  • まずはstartしてみます
yarn start
  • ブラウザに表示されます

image.png

yarn remove @testing-library/react @testing-library/jest-dom && yarn add @testing-library/react @testing-library/jest-dom

初期表示へのテスト

  • テストコードは以下です。
import { render, screen, getDefaultNormalizer } from '@testing-library/react'
import userEvent from "@testing-library/user-event";
import { Provider } from 'react-redux';
import { store } from '../app/store';
import App from '../App';

test('first render', () => {
  render(
    <Provider store={store}>
      <App />
    </Provider>
  );

  expect(screen.getByText('-')).toBeInTheDocument()
  expect(screen.getByRole('button', {name: 'Decrement value'})).toBeInTheDocument()

  const minusButton = screen.getByRole('button', {name: 'Decrement value'})
  expect(minusButton.nextElementSibling?.textContent).toBe("0")

  expect(screen.getByText('+')).toBeInTheDocument()
  expect(screen.getByRole('button', {name: 'Increment value'})).toBeInTheDocument()

  const input = screen.getByRole('textbox', {name: 'Set increment amount'})
  expect(input).toBeInTheDocument()
  expect(input).toHaveValue("2")

  expect(screen.getByRole('button', {name: 'Add Amount'})).toBeInTheDocument()
  expect(screen.getByRole('button', {name: 'Add Async'})).toBeInTheDocument()
  expect(screen.getByRole('button', {name: 'Add If Odd'})).toBeInTheDocument()

  // test Text in App.tsx
  expect(screen.getByText(/Edit/)).toBeInTheDocument();
  expect(screen.getByText(/src\/App.tsx/, {normalizer: getDefaultNormalizer({ trim: false })}  )).toBeInTheDocument();
  expect(screen.getByText(/and save to reload./, {normalizer: getDefaultNormalizer({ trim: false })}  )).toBeInTheDocument();
  /* OR */
  // const Element = screen.getByText(( _content: string, node: Element | null ): boolean => {
  //   return node ? /^Edit src\/App\.tsx and save to reload\.$/.test(node.textContent || '') : false
  // });
  // expect(Element).toBeInTheDocument()
  expect(_getByNestedText('Edit src\/App\.tsx and save to reload\.')).toBeInTheDocument();
  
  expect(_getByNestedText('Learn React, Redux, Redux Toolkit, and React Redux')).toBeInTheDocument();

  expect(screen.getByText('React').closest('a')).toHaveAttribute('href', 'https://reactjs.org/')
  expect(screen.getByText('Redux').closest('a')).toHaveAttribute('href', 'https://redux.js.org/')
  expect(screen.getByText('Redux Toolkit').closest('a')).toHaveAttribute('href', 'https://redux-toolkit.js.org/')
  expect(screen.getByText('React Redux').closest('a')).toHaveAttribute('href', 'https://react-redux.js.org/')
})

/** 
 * A custom query of getByText.
 * It gets a element has full text in nested tags.
 * 
 * parameter
 *  text: should be escaped for Regex
 */ 
const _getByNestedText = (text: string): HTMLElement => {
  const regex = new RegExp("^" + text + "$")
  return screen.getByText(( _content: string, node: Element | null ): boolean => {
    return node ? regex.test(node.textContent || '') : false
  })
}

要素の検索

  • getByTextで該当テキストを拾えるわけですけど、同じテキストが複数ある場合は「どこの」テキストか特定が必要ですね。
  • 画面上は繋がったテキストに見えてもHTML上はタグの入れ子になっている場合、少し工夫が必要でした。
    • getByTextに関数を渡す形でカスタムクエリーを作成しました。

初期表示後のユーザアクションへのテスト

  • テストコードは以下です。
test('user action', async () => {
  render(
    <Provider store={store}>
      <App />
    </Provider>
  );

  /* <span> 0 </span> */
  // click minus button
  const minusButton = screen.getByRole('button', {name: 'Decrement value'})
  for (let i=0; i<3; i++){
    userEvent.click(minusButton)
  }
  expect(minusButton.nextElementSibling?.textContent).toBe("-3")

  /* <span> -3 </span> */
  // click plus button
  const plusButton = screen.getByRole('button', {name: 'Increment value'})
  for (let i=0; i<6; i++){
    userEvent.click(plusButton)
  }
  expect(plusButton.previousElementSibling?.textContent).toBe("3")

  /* <span> 3 </span> */
  /* <input value="2" /> */
  // change the value of text box 
  const input = screen.getByRole('textbox', {name: 'Set increment amount'})
  userEvent.type(input, "5")
  expect(input).toBeInTheDocument()
  expect(input).toHaveValue("5")

  /* <span> 3 </span> */
  /* <input value="5" /> */
  // click [Add Amount] button
  const addAmountButton = screen.getByRole('button', {name: 'Add Amount'})
  userEvent.click(addAmountButton)
  expect(plusButton.previousElementSibling?.textContent).toBe("8")

  /* <span> 8 </span> */
  /* <input value="5" /> */
  // click [Add Async] button
  const addAsyncButton = screen.getByRole('button', {name: 'Add Async'})
  userEvent.click(addAsyncButton)
  // counterAPI will take 0.5 seconds and change the state.
  await new Promise(resolve => setTimeout(resolve, 1000))
  expect(plusButton.previousElementSibling?.textContent).toBe("13")

  /* <span> 13 </span> */
  /* <input value="5" /> */
  // click [Add If Odd] button
  const addIfOddButton = screen.getByRole('button', {name: 'Add If Odd'})
  userEvent.click(addIfOddButton)
  expect(plusButton.previousElementSibling?.textContent).toBe("18")  //from 13
  // click [Add If Odd] button
  userEvent.click(addIfOddButton)
  expect(plusButton.previousElementSibling?.textContent).toBe("18")  //from 18 (no change)

})

ユーザアクション

  • ボタンクリックなどfireEventでも可能ですが、user-eventの方がkeydownなどユーザが実際に起こすアクションに則しているそうです。

非同期処理の結果を待つ

  • [Add Async]ボタンは0.5秒かかるように実装されていますので、ボタン押下後に1秒待ってから結果を確認しました。
    • sleep()はJSに無いのでawaitで待つようにしています。
    await new Promise(resolve => setTimeout(resolve, 1000))
    
    
  • 非同期処理結果で新たな要素が描画されるならfindBy***()で探索すればいいかもです。

以上です

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?