📔

「単体テストの考え方/使い方」のアウトプット ~Part2~

2023/08/05に公開

本記事について

Vladimir Khorikov著作の「単体テストの考え方/使い方」を読むにあたり、内容を咀嚼しながらアウトプットすることを目的とする。

引用

https://amzn.asia/d/3BwaVR2

前回

https://zenn.dev/syun43/articles/156b26291881c1

第1部 単体テストとは

第一章 なぜ、単体テストを行うのか?

1.3.1 コード網羅率について

コード網羅率はテスト網羅率とも呼ばれ、以下のようにして求められる。

コード網羅率=\frac{実行されたコード行数}{総行数}

コード網羅率の高さがテストスイートの質の高さと結びつくわけでは無い。

以下の例のでは、isCheckNumberの実行結果はfalseを返すのでテストに成功する。
ただ、ifの中身に関してはテストコードで実行されない。
総行数が7行で実行されたコード行数は6行なので、コード網羅率は 0.86 = 86% となる。

const isCheckNumber = (num: number) => {
  if (num > 5) {
    return true;
  } else {
    return false;
  }
};

const result = isCheckNumber(2);
expect(result).toBeFalsy();

次の例ではif-elseの代わりに三項演算子を用いている。
総行数が3行で実行されたコード行数は3行なので、コード網羅率は 100% となる。

const isCheckNumber = (num: number) => {
  return num > 5 ? true : false;
};

const result = isCheckNumber(2);
expect(result).toBeFalsy();

isCheckNumber関数の処理はどちらも変わらないにも関わらずコード網羅率に変化が出るため、テストスイートの質が良いという材料にはならない。

1.3.2 分岐網羅率について

分岐網羅率は、コード行数ではなく分岐(if や switch 文)に目を向ける。

分岐網羅率は以下の計算式で算出される。

分岐網羅率=\frac{経由された経路の数}{分岐経路の総数}

コード網羅率のコードを見てみると、分岐が二つあることが分かる。
今回のテストではfalseを返すテストしか実行していないので、分岐網羅率は 1/2 = 50% となる。

const isCheckNumber = (num: number) => {
  return num > 5 ? true : false;
};

const result = isCheckNumber(2);
expect(result).toBeFalsy();

1.3.3 網羅率に関する問題

分岐網羅率がコード網羅率より正確な計測結果を出すことが出来たが、どちらの計測を行なったとしても網羅率を使ってテストスイートの質を評価することが以下の理由により出来ない。

  • 網羅率からは実際にテスト対象のコードが検証されたか保証出来ないため
  • 網羅率の算出をする際に、使用するライブラリ内のコードは計測の対象から外れるため

網羅率からは実際にテスト対象のコードが検証されたか保証出来ない

テスト対象のコードによって生み出された結果が想定していた結果一致することを確認しなければならない。
→ 計測した網羅率に意味を持たせるには、すべての結果を検証しなければいけない

次の例ではisCheckNumber関数の結果をstateで管理するようにした。
テストではisCheckNumber関数の結果しか見ていないにも関わらず、網羅率は先ほどの例と同じ。

const [result, setResult] = useState();

const isCheckNumber = (num: number) => {
    const result =  num > 5 ? true : false;
    setResult(result);
    return result;
};
  
const result = isCheckNumber(2);
expect(result).toBeFalsy();

部分的にしか検証されないテストを極端にしたものが確認不在のテストという。

網羅率の算出をする際に、使用するライブラリ内のコードは計測の対象から外れる

テスト対象のコードがライブラリのメソッドを呼び出していた場合、そのライブラリ内のコードは網羅率の算出から外れる。

例えば文字列型の整数を数値にするライブラリがあった時に、以下のようなテストを書くとする。
しかし、nullや空文字、整数ではない文字列等が引数に与えられた時を検証しておらず十分とは言えない。

const result = Parse.toNumber('5');
expect(result).toEqual(5);

1.3.4 網羅率の結果に縛られること

網羅率の結果に縛られると開発の妨げになる場合がある。
→ 網羅率を最大限に活用するには目標とするような使い方をせず、テストが十分に行われていないものとして見る

網羅率を指定した数値以上になることを強要すると、単体テストで達成しようとしている目標から遠ざかる。

なぜなら、開発者は目標に到達する方法を求めるようになり、確認しなければならないテストのことに意識を向けれなくなるから。

結果、何も確認せず終えるようなテストケースが作られ、テストの価値が無くなる。

1.4 何がテストスイートの質を良くするのか?

テストスイートの評価に関して最も信頼できる方法は、テストケースを一つづつ評価すること。

ただ実際は、テストスイートの質を自動的に評価する方法は無い。
→ 結局は、テストスイートの質は個人的な判断に基づいて行われる

優れたテストスイートには以下の特徴がある。

  • テストすることが開発サイクルの中に組み込まれている
  • コードベースの特に重要な部分飲みがテスト対象となっている
  • 最小限の保守コストで最大限の価値を生み出すようになっている

1.4.1 テストすることが開発サイクルの中に組み込まれている

テストが常に実施される場合にのみテストの自動化を行う。
→ テストの実施が開発サイクルの中に組み込まれていなくてはいけない

理想なのは、コードに変更が加わるたびにテストが実施されること。

1.4.2 コードベースの特に重要な部分飲みがテスト対象となっている

全てのテストケースが平等に作られているわけではないのと同じで、コードベースの全ての部分が単体テストをする価値を持っているわけでは無い。

重要なのは、単体テストにかける労力をシステムにとって非常に重要な部分に向けるということ。
そこまで重要では無い部分は、簡易に、または間接的に検証を行う。

ほとんどのアプリケーションにおいて重要な核となる部分はビジネスロジックを含む部分、つまりドメインモデル。

ドメインモデルに目を向ける中で重要なことは、ドメインモデルをコードベースの本質ではない部分から切り離すということ。
→ 単体テストにかける労力をドメインモデルにのみに向けられるようになるから

1.4.3 最小限の保守コストで最大限の価値を生み出すようになっている

この部分は単体テストにおいて最も難しい部分。

最小限の保守コストで最大限の価値を生み出すようにするためには以下のことを行えなくてはいけない。

  • 価値のあるテストケースを認識できること(価値の低いテストケースも認識できる)
  • 価値のあるテストケースを作成できること

価値のあるテストケースを認識するためには、テストケースの価値を評価するための基準となる枠組みを知っていなければならない。

価値のあるテストケースを作成するためには、上記に加えて設計のテクニックについても理解していなければならない。

1.5 本書から学べること

テストスイートに含まれるテストケースを分析する際の基準となる枠組みについて学び、その枠組みを使ってどのようにテストケースを分析するのか見ていく。

また、以下の項目についても確認してく。

  • テスト対象となるプロダクションコードと共にテストスイートをリファクタリングするにはどうするのか?
  • 異なる手法の単体テストをどのように適用するのか?
  • どのような統合テストを使って、システムの全体的な振る舞いを検証するか?
  • 単体テストにおけるアンチパターンをどのよう識別し、そして回避するか?

next...

Discussion