技術系

JESTでテスト

技術系

こんにちは、なかにしです。

今回はJESTでTESTをしていきます!
言いたかっただけです!!すみません!!

Jestとは

JavaScriptの「ユニットテスト(単体テスト)ツール」です。
Facebookがオープンソースで作成しているので、安心 & 手軽です。

expect(計算内容).toBe(予想結果)のような
直感的に分かりやすい関数名で、サクッと書くことができます。

準備

フォルダを作成し、Jestをインストールします。

今回は「sample-test」フォルダを作成しました。

続いて、Jestをインストールするため、package.jsonを作ります。
cdコマンドで「sample-test」内に入り、以下コマンドを打ちます。

yarn init -y

npm init でも良いのですが、なんとなくyarnにしました。
yarnはnpmよりちょっと高性能なパッケージ管理ツールです。

詳しくはこちら

正直今回はどっちでもいいですが、
なんかプロ感がでるので最近はyarnを使いがちです。

続いて、jestをインストールします。

yarn add --dev jest

続いて、test用のファイルを用意します。
今回は「keisan.js」「keisan.test.js」にしました。

※テスト用ファイルの命名は、「テスト対象.test.js」にします。

テストを書いてく

「keisan.js」にテスト対象の計算を書いていきます。

const keisan = (a, b) => {
  return a + b;
};

module.exports = keisan;

a,bの2つの変数を受け取り、足し算結果を返すものですね。
「keisan.test.js」で使用したいので、exportもしておきます。

続いて「keisan.test.js」にテストを書いていきます。

const keisan = require("./keisan");

test("計算いくぜ!!!", () => {
  expect(keisan(1, 2)).toBe(3);
});

「keisan関数」を「keisan.js」からインポート、
それを使ってテストをしています。

今回はテスト用の関数として、test,expect,toBeの3つを使用しました。
文法はそれぞれ以下です。

▽test関数

test("テスト概要", () => {テスト内容})

「テスト概要」はテスト内容につける名前みたいなものです。

▽expect関数, toBe関数

expect(テスト処理).toBe(予想結果)

expectの()内にテスト処理を書き、toBeの()内に予想結果を書くことで
予想と実際の結果を比較します。

テストを実行

package.jsonに実行用のコマンドを書きます。
公式推奨の方法に則り、scriptsにtestという名前で記載します。

{
  "name": "sample-test",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "jest": "^29.5.0"
  },
  "scripts": {
    "test": "jest"
  }
}

これで「npm test」 or 「yarn test」でjestが実行できるようになりました。

もちろん今回はyarnを使います。

yarn test

▽テスト完了!ちゃんと通ってますね。

React × Jest(Lv.1)

より実務に寄せ、ReactでJestを書いていきます。

▽適当な名前をつけてひな形作成

npx create-react-app jest-test

create-react-appの方だとJestがすでに使えるようになってるんですね、知らなかた…

別途インストールも不要で、
ご丁寧にscriptsにも「npm run test(yarn run test)」で実行できるよう書いてくれています。

試しに npm run test をしてみると…

テスト結果が出ました!

テスト対象のApp.test.jsを見てみます。

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();
});

ザ・Jestですね。

先ほど書いたテストと違う点は、
・render()でテスト対象のコンポーネントを読み込んでいる
くらいですね。

今回の 「/learn react/i」 の部分もそうですが、
結構正規表現の知識って出てきますよね。

iはinsensitive(鈍感)で、大文字小文字を区別しない、という意味です。
今回「App.tsx」に書かれている文字は「Learn React」なので、このiがないとテストに通らないはずです。

▽iを消してやってみる

無事、通りませんでした。
失敗したときに何か変な音が鳴ります。

React × Jest(Lv.2)

新しくコンポーネントを作成し、テストを書いてみます。

今回は「Sugoi」コンポーネントを新たに作成します。すごい!

import "./App.css";
import { Sugoi } from "./Sugoi";    // Sugoiを追加

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
        <Sugoi />    // Sugoiを追加
      </header>
    </div>
  );
}

export default App;
export const Sugoi = () => {
  return (
    <div>
      <p>めっちゃすごい。すごすぎ。Sugoi!</p>
      <button>すごいボタン</button>
    </div>
  );
};

意気揚々と↓のように書いたら怒られました。

import { render, screen } from "@testing-library/react";
import { Sugoi } from "./Sugoi";

test("should sugoi in element", () => {
  render(<Sugoi />);
  expect("sugoi").toBeInTheDocument();
});

恐らくJSX上の「sugoi」文字列に対してテストを実行するつもりなのに、
ただの「sugoi」という文字列を条件にしてるけど大丈夫?的なエラーだと思います。

ちゃんとscreen関数を使用し、JSXとして取得して実施します。

import { render, screen } from "@testing-library/react";
import { Sugoi } from "./Sugoi";

test("should sugoi in element", () => {
  render(<Sugoi />);
  const element = screen.getByText(/sugoi/i); ※('Sugoi')でも可
  expect(element).toBeInTheDocument();
});

無事通りました!
何か資格の試験に合格したみたいで嬉しいですね。

React × Jest(Lv.Max)

実用の為、もう少し細部まで理解します。

render関数でコンポーネントを読み込んだ後、
screen.debug()で描画されるコンポーネントの中身が見れます。

import { render, screen } from "@testing-library/react";
import { Sugoi } from "./Sugoi";

test("should sugoi in element", () => {
  render(<Sugoi />);
  screen.debug();
});

文章が入ってるか否か系はこれでざっと確認ができます。
機能面(onClickなど)はこれだと分からないので、テストで確かめる必要があります。

onClickでカウントアップするようなコンポーネントに改変し、テストします。

import { useState } from "react";

export const Sugoi = () => {
  const [sugoCount, setSugoCount] = useState(0);

  const sugo = () => {
    setSugoCount(sugoCount + 1);
  };

  return (
    <div>
      <p>めっちゃすごい。すごすぎ。Sugoi!</p>
      <p>すごカウント:{sugoCount}</p>
      <button onClick={sugo}>すごいボタン</button>
      <button onClick={sugo}>すごいボタン2</button>
    </div>
  );
};
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Sugoi } from "./Sugoi";

test("should text change when click button", async () => {
  render(<Sugoi />);

  // すごいボタンを取得
  const button = screen.getByRole("button", { name: "すごいボタン" });

  // すごカウント:0があるかチェック
  expect(screen.getByText("すごカウント:0")).toBeInTheDocument();
  
  // ボタンクリック
  await userEvent.click(button);
  
  // ボタンクリック後、すごカウント:1になるかチェック
  expect(screen.getByText("すごカウント:1")).toBeInTheDocument();
});

▽無事通りました。

ちなみに、以下の行ですが

const button = screen.getByRole("button", { name: "すごいボタン" });

▽これだと通りません

const button = screen.getByRole("button");

理由として、getByRoleは「一個しか取れず、2個以上だとエラーになる」からです。
今回はボタンを2個作ったので、エラーになります。

さいごに

いかがでしたでしょうか。

テストは結構面倒な作業ではありますが、
後々の保守の際、事前に書いたテストを実行するだけである程度の規律が保証されます。

今後、大規模案件や慎重を期す案件(金融など)に参画する際は
積極的に取り入れて長期的な品質の確保をしていくのをオススメします。

ベンチャーやスタートアップはいらないと思います!!!!