カバレッジは数値だけを見るのではなく、意味のあるテストづくりの参考として活用しようね

カバレッジは数値だけを見るのではなく、意味のあるテストづくりの参考として活用しようね

logo 雑ポエム

投稿日: 2022年12月02日

テスト

※ カバレッジにはいくつかの種類がありますが、今回はそれを全部ひっくるめてカバレッジと呼ぶことにしています

よくある勘違い

カバレッジの話になると

とりあえずgoogleが85%以上を推奨してるからカバレッジを85%に上げよう !!!そうなればバグが少なくなる!!!!」 となり、脳死でカバレッジを上げることが目標になったりします。しかし、カバレッジの数値はあくまでも

「ファイル全体に対して実行された行の割合」だったり

「条件文に含まれる条件全体に対して実行された条件パターンの割合」だったり

...etc(カバレッジもいくつか種類があるので省略)

というものであり。「カバレッジが高い=テストの品質が高い」というわけでは決してありません。テストを書くときは常に「何をテストするのか」「何を保証するのか」を考えて書く必要があります。

テストの品質が高い -> カバレッジが85%以上になってる

が真であっても

カバレッジが85%以上になっている -> テストの品質が高い

というわけではない。ということですね!

ではカバレッジは意味ないことなのか??

そういうことは言ってないですよ!!!

ただ、カバレッジの「 数値だけ 」に固執し続けると、質の低いテストを書くことになります。今回はそれを実際にハンズオン形式でお伝えすることができればなと思います。

カバレッジが高いことが質の高いテストではないということの例

自分はキラキラでフレッシュなフロントエンドエンジニア(ニチャァァ)なので、今回はReact + Jest + React Testing Libraryでテストを実行していきます

まずは環境構築です

npx [email protected] app

appフォルダの中に入りましょう。今回はsrc/App.jsとApp.test.jsを編集していきます。

App.jsとApp.test.jsは以下のようになっています

  // App.js
import logo from './logo.svg';
import './App.css';


function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}
  
  export default App;


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

このテストのカバレッジを収集してみましょう。以下のコマンドでカバレッジを取得してください

  ❯ npm test -- App.test.js --coverage --watchAll=false

> [email protected] test
> react-scripts test

 PASS  src/App.test.js
  ✓ renders learn react link (16 ms)


--------------------|---------|----------|---------|---------|-------------------
File                | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------------|---------|----------|---------|---------|-------------------
All files           |    8.33 |        0 |   33.33 |    8.33 |                   
 App.js             |     100 |      100 |     100 |     100 |                   
 index.js           |       0 |      100 |     100 |       0 | 7-17              
 reportWebVitals.js |       0 |        0 |       0 |       0 | 1-8               
--------------------|---------|----------|---------|---------|-------------------
  

素晴らしい!!!App.jsを見てください。すべてカバレッジが100%になっています!これは非常に品質の高いテストでしょうね!!!!

じゃあ次はApp.test.jsを編集してみましょう!

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

実行してみましょう。

   PASS  src/App.test.js
  ✓ renders learn react link (15 ms)


--------------------|---------|----------|---------|---------|-------------------
File                | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------------|---------|----------|---------|---------|-------------------
All files           |    8.33 |        0 |   33.33 |    8.33 |                   
 App.js             |     100 |      100 |     100 |     100 |                   
 index.js           |       0 |      100 |     100 |       0 | 7-17              
 reportWebVitals.js |       0 |        0 |       0 |       0 | 1-8               
--------------------|---------|----------|---------|---------|-------------------
  

素晴らしい!!!100%のままですね!!天才!!

...

...…?

そんなわきゃない。

今回は例が極端でしたが、カバレッジの数値だけにこだわりすぎると意味のないテストが大量に生成され、しかも「このファイルはカバレッジ100%だからとりあえず大丈夫っしょ!」となり、修正されないまま「質の高いテスト」として居続けることになるでしょう

じゃあどんなテストを書けばいいのよ

それはこのファイル。このコンポーネントに何を保証してほしいのか。逆になにが変わってても問題ないか、を切り分ける必要があります。

App.jsにあるコンポーネントの特徴を箇条書きで列挙していきましょう

  1. logo.svgという画像が表示されている
  2. 「Edit src/App.js and save to reload.」という文字が表示されている
  3. 「Learn React」という文字があり。それをクリックすると別タブで「https://reactjs.org」が開く
  4. コンポーネント内にuseStateやuseEffectなどのロジックはない

過剰にテストを追加しすぎると微細な修正をしただけでテストが失敗し、生産性が大幅に下がりますし、じゃあテストを書かなすぎても想定されていない変更を検知できずにバグをリリースしてしまう事になったりします。この境界線は自分で考えましょう。

とりあえず今回はこのコンポーネントには3.を保証するテストを書きます。僕は3だけでしたが、あなたは2.と3.両方を保証してもいいし、1だけを保証してもいいのです。

  test("Appコンポーネントを描画し、Learn Reactをクリックすると別タブでhttps://reactjs.orgを開く", () => {
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeVisible();
  expect(linkElement).toHaveAttribute("href", "https://reactjs.org");
  expect(linkElement).toHaveAttribute("target", "_blank");
  expect(linkElement).toHaveAttribute("rel", "noopener noreferrer");
});
  

これでApp.jsは「Learn React」をクリックすると別タブでhttps://reactjs.orgを開くことが保証されたことになります。

逆にそれ以上のことは保証されないので、実装が変更されても検知できません

実際に描画されている内容が変わっていないかを確認するには、スナップショットテストか、ビジュアルリグレッションテストで確認することを推奨します。

(横道にそれたけれど)カバレッジを有効活用してみよう

App.jsの内容を以下のように編集してください

  import logo from "./logo.svg";
import "./App.css";
import { useEffect, useState } from "react";


function App() {
  const [state, setState] = useState(0);


  const handleClickPlus = () => {
    if (state >= 3) {
      return;
    }
    setState((rest) => rest + 1);
  };


  const handleClickMinus = () => {
    if (state <= 0) {
      return;
    }
    setState((rest) => rest - 1);
  };


  useEffect(() => {
    if (state === 3) {
      alert("3になりました");
    }
  }, [state]);


  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
        <div>
          <button onClick={handleClickPlus}>+</button>
          <button onClick={handleClickMinus}>-</button>
          <p>{state}</p>
        </div>
      </header>
    </div>
  );
}


export default App;

  

今回の修正で以下の機能が追加されています

  • +ボタンと-ボタンが追加されている
  • stateの内容が表示されている
  • +ボタンをクリックするとstateが1増加する。3以上の値の場合は増加しない
  • -ボタンをクリックするとstateが1減少する。0以下の場合は減少しない
  • stateが3の場合はalert("stateが3になりました")が実行される

それではこれをテストしていきましょう。3. では「Learn React」という文字があり。それをクリックすると別タブで「https://reactjs.org」が開くが保証されているので。このテストは通過します

  ❯ npm test -- App.test.js --coverage --watchAll=false




> [email protected] test
> react-scripts test


 PASS  src/App.test.js
  ✓ Appコンポーネントを描画し、Learn Reactをクリックすると別タブでhttps://reactjs.orgを開く (29 ms)


--------------------|---------|----------|---------|---------|-------------------
File                | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------------|---------|----------|---------|---------|-------------------
All files           |   23.07 |       10 |      25 |      25 |                   
 App.js             |      40 |    16.66 |   33.33 |   46.15 | 9-12,16-19,24     
 index.js           |       0 |      100 |     100 |       0 | 7-17              
 reportWebVitals.js |       0 |        0 |       0 |       0 | 1-8               
--------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
  

おっとっと?カバレッジがかなり低くなっていますね・・・・これではApp.jsの機能が全然実行されていないですよね。流石に追加でテストを書かないといけませんね。

ここでカバレッジの数値だけ上げればいいと思う人は以下のテストを書いてください

  test("App.jsのカバレッジを上げる", () => {
  render(<App />);


  const plusButton = screen.getByRole("button", { name: "+" });
  const minusButton = screen.getByRole("button", { name: "-" });


  fireEvent.click(plusButton);
  fireEvent.click(plusButton);
  fireEvent.click(plusButton);
  fireEvent.click(plusButton);


  fireEvent.click(minusButton);
  fireEvent.click(minusButton);
  fireEvent.click(minusButton);
  fireEvent.click(minusButton);
  fireEvent.click(minusButton);
});
  

手元で試してみましょう。これでカバレッジは100%になりますね!

......

ここでようやくカバレッジを活用するときです

先程のクソなカバレッジ100%になる素晴らしいテストを削除して再度テストを実行し、低い状態のカバレッジを出力してください。大抵のテスティングフレームワークにはカバレッジレポートをGUIで確認するための機能が入っています。

jestの場合、デフォルトで./coverage/lcov-report/index.htmlにカバレッジレポートを参照できるhtmlがあります。こいつをブラウザで開いてみましょう。

App.jsを開いてみましょう

テストレポートの細かい見方はドキュメントを自分で見つけてください。とりあえずこのレポートを見れば

handleClickPlus、handleClickMinusが実行されていない useEffect内部のalertが実行されていない ということがわかりますね!

このカバレッジ情報をもとに「意味のあるテスト」をブラッシュアップしていきましょう!

カバレッジは「意味のあるテスト」を作ってくためのツールとして活用していきましょう!!!

最後にこのカバレッジをもとに「意味のあるテスト」を作っていこうと思っています。何を保証するのか?を列挙していきます。あなたはあなたで「何を保証するのか」を一緒に考えてみてください。とりあえず自分は以下を保証しようと思います

  • plusボタンを押すとstateが1増加する
  • minusボタンを押すとstateが1減少する
  • stateが3になるとalertが実行されている(alertの中身の文章は保証しない)
  • stateが3以上の場合増加しない
  • stateが0以下の場合減少しない
  • stateは最初は0になっている
  • 以下の保証する内容をもとにテストを書いていきます
  test("plusボタンを押すとstateが1増加する、minusボタンを押すとstateが1減少する、stateが3になるとalertが実行されている、stateが3以上の場合増加しない、stateが0以下の場合減少しない、stateは最初は0になっている", () => {
  render(<App />);


  // alertが実行されているか確認
  const alert = jest.spyOn(window, "alert");


  const plusButton = screen.getByRole("button", { name: "+" });
  const minusButton = screen.getByRole("button", { name: "-" });


  // stateは最初は0になっている
  const state = screen.getByText("0");
  expect(state).toBeVisible();


  // plusボタンを押すとstateが1増加する
  fireEvent.click(plusButton);
  expect(state).toHaveTextContent("1");


  // minusボタンを押すとstateが1減少する
  fireEvent.click(minusButton);
  expect(state).toHaveTextContent("0");


  // stateが3になるとalertが実行されている
  fireEvent.click(plusButton);
  fireEvent.click(plusButton);
  fireEvent.click(plusButton);
  expect(state).toHaveTextContent("3");
  expect(alert).toHaveBeenCalledTimes(1);


  // stateが3以上の場合増加しない
  fireEvent.click(plusButton);
  expect(state).toHaveTextContent("3");


  // stateが0以下の場合減少しない
  fireEvent.click(minusButton);
  fireEvent.click(minusButton);
  fireEvent.click(minusButton);
  fireEvent.click(minusButton);
  expect(state).toHaveTextContent("0");
});

  

どうでしょうか?おそらく先程の「カバレッジを上げるためだけを目的としたテスト」よりは意味のあるテストが書けたと思います。カバレッジも偶然にも100%になっていました!

まとめ

カバレッジは数値だけを求めのではなく、意味のあるテストを作るためのツールとして活用していきましょうね!!

以上!!!!

© 2023 u-yas All rights reserved.