🗒️

ユニットテストは振る舞いをテストするもので、実装に対して書くものでは無い

2023/05/05に公開

この記事の要約

  • ユニットテストは実装内容に注目せず、実際の用途と入出力に焦点を当てて行う。
  • 1つの振る舞いに対して、1つのテストを行うのであって、1つのコードではない。
  • 必要な振る舞い単位で関数を分けず、機械的な単位分割は避ける。
  • 部分的にテストをしたいからといって、privateをpublicにしない。
  • 「単体テストの考え方/使い方」は実装コードを改善するうえでも、とても良い本。

書籍リンク:単体テストの考え方/使い方
https://book.mynavi.jp/ec/products/detail/id=134252


「単体テストの考え方/使い方」を読んで、
今まで悪いユニットテストを書いていたことが分かり、
同時に良いユニットテストが何かも分かったので、思考を整理するために記事にしました。
どう変わったかを、送料計算を例に説明していきます。

要件

  1. 購入金額に応じて送料を計算する。
  2. 1000円以上の購入で送料無料になる。

読書前の思考パターン(悪い例)

この要件を受けて、次のように考えて実装を組んでいました。

  • 送料を計算する処理を作成するとは別に、無料の概念も必要そう。
  • 送料が無料になる金額は重要なので、テスト出来るようにメソッドに分離しよう

出来上がった実装と、テストコード

class 送料を計算するユースケース {
  // 送料クラスを使った計算
  送料クラス = new 送料クラス();
  送料 = (送料->送料が無料になる購入金額か())
     ? 送料クラス->無料になる()
     : 送料クラス->計算する();
  ・・・
}
   
// 送料クラスの実装
class 送料 {
  public function 送料が無料になる購入金額か(購入金額): bool {
    return (購入金額 >= 1000);
  }
  public function 計算する(): 送料 { return 送料を計算する; }
  public function 無料になる(): 送料 { return 0; }
}

// 送料クラスのテスト
class 送料Test extends TestCase
{  
 // 送料が無料になる購入金額か()のテスト
  test購入金額が1000円以上の場合、trueを返す()
  test購入金額が1000円未満の場合、falseを返す()
  
  // 計算する()のテスト
  test送料を計算する()
  test購入金額が一定額以上なら、送料は計算されない()
}

このコードの何が悪いのか

複数のメソッドを呼び出さないと、要件が満たせない

本来1つであった送料を計算する、といった要望を実現するために、
3つのメソッドが必要になっています。
実行順序も気を付ける必要が出てきます。

一見、要件にない要件が増えたように見える

真偽値を返すことや、送料を計算するといった内容がテストに表れていますが、実際の要件にはそのような指定はありません。
本来は2つであった要件が、実装段階で増えたように見えてしまいます。

別の関心ごとをテストしているように見える

送料のテストなのに、購入金額のテストをしているようにも見えてしまいます。
これは送料無料の判定と、送料計算の関数が分かれていて、テストも分離しまっているため起こっています。

テスト名がメソッド名に引っ張られている

送料が無料になる購入金額か()、のメソッドでは、
判定要素の購入金額に引っ張られ、購入金額が前に出てしまっています。
計算する()のテストでは、
計算する、計算されない、のような回りくどい表現になっています。


改善案

冒頭に書いた要約の内容も踏まえて、次のように修正します。

  • 1メソッド(1API)で送料を計算するように、1単位の振る舞いに纏める
  • 振る舞いに対してテストを書く
class 送料を計算するユースケース {
  // 送料クラスを使った計算
  送料クラス = new 送料クラス();
  送料 = 送料クラス->計算する();
  ・・・
}

// 送料クラスの実装
class 送料 {
  public function 計算する(購入金額): 送料 {
    return (購入金額 >= 1000)
      ? 0: 計算した送料;
  }
}

// 送料クラスのテスト
class 送料Test extends TestCase
{  
  test送料を計算する()
  test購入金額が1000円以上なら、送料は無料になる()
}

改善点

1メソッドで要件が満たせた

このメソッドの使い方なら間違いようがありません。

コードの用途がはっきりした

メソッド名とテストケースからも、
送料を計算していることがはっきりと読み取れるようになりました。
また、送料を計算するさいに必要な関心ごとが一か所に纏まりました。

送料に関するテストをしていることが分かる

テストケースに「送料」の文言がはっきり打ち出されているため、
関心ごとがぶれていません。

テスト名が振る舞いに沿った名前になった

送料が計算されない⇒送料が無料になる
と、より用途に沿ったテスト名になりました。

まとめ

今までテストは書けているけれど、今一つ質が改善されたり、
効果が薄いと感じることが多かったです。
しかし「単体テストの考え方/使い方」を読んでいくなかで、
「良い」の定義を定めることが出来ました。
とても良い本なので、迷っている人は買いましょう。
書籍リンク:単体テストの考え方/使い方
https://book.mynavi.jp/ec/products/detail/id=134252

Discussion