期待値が予想できるようなテストコードにする

概要

初心者のうちは、コード量が多いわけでもないのに、なぜかぱっと見ただけではよくわからない不思議なコードを書いてしまうものだ ()

まずは、そんなコードを振り返ってみよう。

まえおき

ここに登場するコードは以下で実装されたものを前提としている。

例やコードは、サンプル向けにアレンジしたもののため、違和感はご愛嬌。

やりたかったこと

以下のようなメソッドの UT (ユニットテスト) を実装したかった。

書籍に紐付く章の「1番最後に追加された章の章番号」を取得するメソッドがあるとしよう。
たとえば、「猫の吸い方」という本には以下のような章があり、1章から順に3章まで章レコードを作成した場合、 1番最後の章にあたる「第3章 おわりに」の章番号 3 を取得するようなメソッドである。

  • 第1章 はじめに
  • 第2章 猫の上手な吸い方
  • 第3章 おわりに

実装コードは以下。


class ChapterClass:
  """章クラス"""

  def return_last_chapter_num(self, book_id: int) -> int:
      """書籍idに合致する章のうち、1番最後に追加された章の章番号を返す

      :param book_id: 検索対象の書籍id
      :return: 書籍idに合致する章のうち、1番最後に追加された章の章番号
      """

      chapter = Chapter.objects.filter(book_id=book_id).last()

      return chapter.num

このメソッドの UT として、以下のようなテストコードを実装した。


@pytest.fixture
def book(self):
    return BookFactory()

@pytest.fixture
def chapter(self, book):
    return ChapterFactory(book=book)
...

def test_return_last_chapter_num(self, book, chapter):
    """書籍idに合致する章のうち、1番最後に追加された章の章番号を返すこと"""

    # arrange
    ChapterFactory(book=book)
    ChapterFactory(book=book)

    # act
    actual = ChapterClass.return_last_chapter_num(book_id=book.id)

    # assert
    assert actual == 3

このコードの問題点

上記のコードには、以下のような Not Good がある。


  • chapter を引数で受け取っているが、テストコード内で使われていない
    • → 何のために引数で受け取っているのかが、ぱっと見でわからない
  • book を引数で受け取っているが、fixture の book を使う必要性が薄い
    • → テストコード内で BookFactory() すればよいはずだが、それだと不都合があるのだろうか? という疑問が湧いてしまう
  • テストコード内で Chapter を2つ生成しているので、期待値も 2 となるはずだが、なぜか 3 になっている
    • → 期待値が予想と異なる

なぜこうなった?

「せっかく fixture があるのだから積極的に使おう!」と思ったのでしたまる。

どうするとよさそうか?

fixture を使うことでわかりにくいコードになるのであれば、無理に使う必要はない。
今回の場合は fixture を使う必要性は薄いため、テストコード内で必要なオブジェクトを生成し、 期待値が予想できるようにすると Good。


def test_return_last_chapter_num(self):
    """書籍idに合致する章のうち、1番最後に追加された章の章番号を返すこと"""

    # arrange
    book = BookFactory()
    ChapterFactory(book=book)
    ChapterFactory(book=book)
    ChapterFactory(book=book)

    # act
    actual = ChapterClass.return_last_chapter_num(book_id=book.id)

    # assert
    assert actual == 3

以下のように create_batch() を使うのもよさそう。
コード行が減って見た目にもスッキリし、size を見れば生成されるオブジェクトの数がすぐにわかるので可読性も Good。


def test_return_last_chapter_num(self):
    """書籍idに合致する章のうち、1番最後に追加された章の章番号を返すこと"""

    # arrange
    book = BookFactory()
    ChapterFactory.create_batch(book=book, size=3)

    # act
    actual = ChapterClass.return_last_chapter_num(book_id=book.id)

    # assert
    assert actual == 3

おわり

よかれと思いやってみた結果、期待値が直感に反するテストコードに仕上がってしまった例を振り返ってみた。
このままマージしていたら、本人も数日後には不思議コードの読み解きに困っていただろう。

レビュアーに感謝。

参考