LoginSignup
13
13

JestでReact(TypeScript)のテストしてみた

Posted at

:pushpin: はじめに

Reactで個人的に色々作ったり勉強することが多く、「テストまで回す癖つけた方がいいよ!」というアドバイスをいただいたので、初めてのReactでのテストの備忘録として書いてます。なので初歩的な内容です。。。

:pushpin: Jestとは

それぞれのプログラミング言語には、テストを補助するためのフレームワークが存在します。
Jestは、JavaScriptのテストフレームワークです。
Babel、TypeScript、Node、React、Angular、Vue などの様々なフレームワークを利用したプロジェクトで動作します!(公式引用)

Jest公式:https://jestjs.io/ja/

:pushpin: とりあえず、テストファイルを見てみる

① Reactのプロジェクトを作る

npx create-react-app my-app --template typescript

プロジェクトを作ると、自動的に

  • App.test.tsx
  • setupTests.ts

というファイルが作成されます。

② App.test.tsxを見てみる

App.test.tsxには既に、下記のようなコードが書かれています。

App.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});

上記では、Appコンポーネントのテストを行っています。

③ setupTests.tsを見てみる

テストの初期化設定をするファイルです。
create react appのサイトにsetupTests.jsについて書いてあります。

この機能は、react-scripts@0.4.0 以降で利用可能です。
アプリがテストでモックする必要のあるブラウザAPIを使用している場合、またはテストを実行する前にグローバルな設定が必要な場合は、プロジェクトに src/setupTests.js を追加してください。これは、テストを実行する前に自動的に実行されます。

setupTests.tsファイルには、下記のように書かれています。

setupTests.ts
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

アサーションとは、あるコードが実行される時に満たされるべき条件を記述して実行時にチェックする仕組み。(←自分がわからなかったので、自分用に記述)

④ テストを走らせてみる

npm test

すると、下記のように表示されるので、とりあえずaキーを押して全てのテストを実行します。

No tests found related to files changed since last commit.
Press `a` to run all tests, or run Jest with `--watchAll`.

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press q to quit watch mode.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press Enter to trigger a test run.

ーーー

// 実行結果
 PASS  src/App.test.tsx
  ✓ renders learn react link (44 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.308 s
Ran all test suites.

Watch Usage: Press w to show more.

元々のソースを何もいじってないので通ります。
wキーを押し、qキーを押してウォッチモードを終了します。

:pushpin: Jestについて理解する

① テストの書き方

test('テストケースの名前', () => {
    // テスト
});

② 関連テストをまとめる

describe()を使うことで、いくつかの関連するテストをまとめたブロックを作成することができます。

describe('ブロック名', () => {
  test('テストケース1', () => {
      // テスト
  });
  test('テストケース2', () => {
      // テスト
  });
});

参考:https://jestjs.io/ja/docs/api#describename-fn

③ テストの条件を定義する

Matcher(マッチャー)を使って、テストの評価条件を定義します。
Matcherはメソッドです。テスト実行時に、どんな結果が返ってきてほしいかを定義して、失敗成功を判定するようです。
下記のように書きます。

expect().マッチャー();

expect(値)は、"expectation" オブジェクトを返します。
expect関数には、テストしたい関数やコンポーネントを渡してあげます。

App.test.tsx
// テストしたい関数
function calcFactorial(num: number):number {
  let result: number = 1;
  for(let i = num; i > 0;i--) {
      result = result * i;
  }
  return result;
}

// テスト
test('/utils/calc.tsのcalcFactorial関数のテスト', () => {
  const num: number = 4;

  console.log(calcFactorial(num));
  expect(calcFactorial(num)).toStrictEqual(24);
});

Matcherの一覧やそれぞれのメソッドの使い方をわかりやすくまとめてくれているサイトがたくさんありますので、そちらを参考にしてみてください。

:pushpin: テスト書いてみる

フォルダ構成は以下のようにしました。

src
 L App.css
 L App.test.tsx
 L App.tsx
 L index.css
 L index.tsx
 L logo.svg
 L react-app-env.d.ts
 L reportWebVitals.ts
 L setupTests.ts
 L __tests__                    <<- 追加
     L list.test.tsx            <<- 追加
 L components                   <<- 追加
     L list                     <<- 追加
         L list.module.css      <<- 追加
         L list.tsx             <<- 追加
 L utils                        <<- 追加
     L calc.ts                  <<- 追加

① TypeScriptの簡易的なテスト

まずは、「TypeScriptで書いた関数が意図した結果を返してくれるかどうか」くらいのテストをやってみます。

まずテスト用の関数の作成

配列を受け取り、配列中に2つ以上発生する数値を除去した配列を返す関数を作ります。(語彙力が乏しくてすみません)

./utils/calc.ts
function removeDuplicates(arr: number[]):number[] {
    let result:number[] = [];

    arr.forEach((num, i) => {
        let count = 0;

        arr.forEach((num2) => {
            if(num === num2) count ++;
        });
        // 配列中に同じ値が1つだけ出現したやつをresult配列に
        if(count === 1) result.push(num);
    });

    return result;
}

module.exports = removeDuplicates;

テストを書く

とりあえず、App.test.tsxに記述していきます。

App.test.tsx
test('/utils/calc.tsのremoveDuplicates関数のテスト', () => {
    const numArr: number[] = [3, 4, 2, 6, 2, 2, 1, 3, 4, 5, 7];
    const removeDuplicates = require('./utils/calc');
  
    console.log(removeDuplicates(numArr));
    expect(removeDuplicates(numArr)).toStrictEqual([6, 1, 5, 7]);
});

Reactでテストって何から書けばいいのか正直わからなかったのですが、
console.logなどで変数の中身を表示する時ってあるじゃない?
エラー出たら出力する為に書いて、解決して消して、またエラー出たから書いて、、、ってやっていると面倒だから、console.logをテストに書いておいて、テスト回して出力確認するっていうのから初めてみたらいいのよん!」
とアドバイスをいただき、個人学習でテストを行うハードルがかなり下がりました:angel_tone2:

結果はこんな感じ↓ wキー押して、qキー押してウォッチモードを終了します。

  console.log
    [ 6, 1, 5, 7 ]

      at Object.<anonymous> (src/App.test.tsx:16:11)

 PASS  src/App.test.tsx
  ✓ renders learn react link (47 ms)
  ✓ /utils/calc.tsのremoveDuplicates関数のテスト (53 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.627 s
Ran all test suites related to changed files.

Watch Usage: Press w to show more.

引っ掛かったらこんな感じ

テスト時に使うMatcher間違えた時のエラーです!
通ったテストと、通らなかったテストがわかりやすいですね!

 FAIL  src/App.test.tsx
  ✓ renders learn react link (46 ms)
  ✕ /utils/calc.tsのremoveDuplicates関数のテスト (27 ms)

  ● /utils/calc.tsのremoveDuplicates関数のテスト

    expect(received).toBe(expected) // Object.is equality

    If it should pass with deep equality, replace "toBe" with "toStrictEqual"

    Expected: [6, 1, 5, 7]
    Received: serializes to the same string

      14 |   const removeDuplicates = require('./utils/calc');
      15 |   
    > 16 |   expect(removeDuplicates(numArr)).toBe([6, 1, 5, 7]);
         |                                    ^
      17 | });

      at Object.<anonymous> (src/App.test.tsx:16:36)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        3.594 s
Ran all test suites related to changed files.

② コンポーネントのテスト

「コンポーネントが意図したように表示されるかどうか」のテストをやってみます。

まずテスト用のコンポーネントの作成

propsでitemsheaderDataを受け取り、リストを表示するコンポーネントです。
headerDataが渡された時は、リストにヘッダーを表示するようにしています。

components/list/list.tsx
import React from "react";

/* import css */ 
import styles from './list.module.css';

type Props = {
    items: {title: string, description?: string, icon?: React.ReactNode}[],
    headerData?: string
}

export default function ListWithIcon(props: Props){

    const {items, headerData} =  props;

    return(
        <ul className={styles.listWrapper}>
            { headerData && <li className={styles.header}> { headerData } </li> }
            {
                items.length === 0 ? 
                    <span>表示するデータがありません</span>
                :
                    items.map((item, index) => {
                        return(
                            <li key={index}>
                                <span className={styles.leftContent}>
                                    {item.icon && 
                                        <span className={styles.iconWrapper}>
                                            { item.icon }
                                        </span>
                                    }
                                    <span className={styles.title}>{item.title}</span>
                                </span>
                                <span>{item.description}</span>
                            </li>
                        )
                    })
            }
        </ul>
    )
}

components/list/list.module.css

.listWrapper {
    padding: 0;
}

.listWrapper li {
    padding: 0.5rem 1rem;
    border-radius: var(--my-border-radius-s);
    background-color: var(--my-color-bgColor);
    list-style: none;
    margin-bottom: 0.5rem;
    color: var(--my-color-navy);
    transition: all 0.5s;
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.listWrapper li:not(.header):hover {
    opacity: 0.5;
    transition: all 0.5s;
}

.listWrapper .header {
    background-color: var(--my-color-navy);
    color: var(--my-color-white);
    border-radius: var(--my-border-radius-s) var(--my-border-radius-s) 0 0;
}

.leftContent {
    display: flex;
    align-items: center;
    justify-content: left;
}

.title {
    font-weight: bold;
    margin: auto var(--my-margin-s);
}
.iconWrapper {
    background-color: var(--my-color-green);
    border-radius: var(--my-border-radius-circle);
    color: var(--my-color-white);
    min-width: var(--my-font-size-h1);
    min-height: var(--my-font-size-h1);
    padding: var(--my-padding-xs);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--my-font-size-p);
}

テストを書いてみる

新しく__tests__の中にlist.test.tsxファイルを追加し、テストを記述します!
今回テストしたい内容は下記になります。
headerDataを渡した場合はリストにヘッダーを表示し、渡さない場合は非表示になるかどうか
なので、
:point_up: headerDataを渡してコンポーネントを描画し、ヘッダーが表示されるか確認するパターン と
:v: headerDataを渡さずにコンポーネントを描画し、ヘッダーが非表示になるか確認するパターン の
2つのテストを記述します!!

/__tests__ / list.test.tsx
import { render, screen } from '@testing-library/react'
import ListWithIcon from '../component/list/list';
import CategoryIcon from '@mui/icons-material/Category';
 

describe('components/listテスト', () => {

    // ListWithIconコンポーネントに渡すデータ
    const listItems: {title: string, description?: string, icon?: React.ReactNode}[] = [
        {title: 'タイトル1'},
        {title: 'タイトル2', description: '説明がきです〜'},
        {title: 'タイトル3', description: 'ディスクリプションです〜', icon: <CategoryIcon fontSize="small" />}
    ]

    /* --  テスト1 -- */
    test('headerDataを渡しヘッダーを表示する', () => {
        // render()関数を使ってコンポーネントを描画(headerDataを渡す)
        const view = render(<ListWithIcon items={listItems} headerData='リストのヘッダーだよ'/>);
        
        // 興味本位でconsole.log
        console.log('view: ', view);
        // 要素の取得(@testing-library/reactのクエリ ` queryByTestId ` を使用)
        const listDom = screen.queryByTestId('list-header');
        // ちゃんと返ってくる
        console.log(listDom);
        // nullにならないことを期待する
        expect(listDom).not.toBeNull();
    })

    /* -- テスト2 -- */
    test('headerDataを渡さずヘッダーを表示しない', () => {
        // render()関数を使ってコンポーネントを描画(headerDataを渡さない)
        render(<ListWithIcon items={listItems} />);
        // 要素の取得(@testing-library/reactのクエリ ` queryByTestId ` を使用)
        const listDom = screen.queryByTestId('list-header');
        // nullが返ってくる
        console.log(listDom);
        // nullになることを期待する
        expect(listDom).toBeNull();
    })

})

特定のDOMを取得には、getByRole()メソッドを使用するのですが、今回自分のコンポーネント定義の仕方が悪くて、リストのヘッダーもそれ以外もliタグで描いちゃってるので、getByRole('listitem')で指定しちゃうと、TestingLibraryElementErrorです: 役割 "listitem" を持つ要素が複数見つかりました。と怒られちゃいました。
複数要素を探す方法もあるのですが、今回はヘッダーにあたるliの表示非表示をテストしたいということで、色々調べて、あまり推奨されてはいませんが、とりあえず挙動を確認したいのでgetByTestId()を使うことにしました。
テストで利用したい要素にdata-testid属性に設定した任意の名前でDOMを取得することができます。

Testing Libraryのクエリについて

sceren.クエリ();

ページ上の要素を見つけるためにTesting Libraryが提供するメソッドです。クエリにはいくつかの種類(「get」、「find」、「query」)があり、それらの違いは、要素が見つからなかった場合にエラーを投げるか、Promiseを返して再試行するかです。どのようなページのコンテンツを選択するかによって、異なるクエリがより適切であったり、より適切でなかったりすることがあります。最もアクセスしやすい方法でページをテストするためにセマンティッククエリを利用する方法に関する推奨事項については、優先順位ガイドを参照してください。

引用:https://testing-library.com/docs/queries/about

const 任意の名前 = render(コンポーネント); とすると、下記のようなエラーが出ました。
戻り値の名前は、viewまたはutilsが推奨されているようです。

任意の名前 is not a recommended name for render returned value. Instead, you should destructure it, or name it using one of: view, or utils

getByTestId()について
Testing Libraryのサイトには、以下のように記載がありました。

In the spirit of the guiding principles, it is recommended to use this only after the other queries don't work for your use case. Using data-testid attributes do not resemble how your software is used and should be avoided if possible. That said, they are way better than querying based on DOM structure or styling css class names. Learn more about data-testids from the blog post "Making your UI tests resilient to change"

指導原則の精神に則り、他のクエリがあなたのユースケースで機能しない場合にのみ、これを使用することが推奨されます。data-testid属性の使用は、あなたのソフトウェアがどのように使用されているかに似ていないので、可能であれば避けるべきです。とはいえ、DOM構造やスタイリングCSSのクラス名に基づいてクエリを実行するよりは、はるかに優れています。data-testidについては、ブログポスト「Making your UI tests resilient to change」で詳しく解説しています。

最後に

次回は、setupTest.tsの記述方法やイベントを使ったテスト、結合テストあたりを記事にできるくらい理解してこようと思います。

参考資料

13
13
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
13
13