この記事は Property-Based Testing Advent Calendar 2018 2日目の記事です。

==> Day1: Property-Based Testing Advent Calendar のイントロダクション

Property-Based Testing (PBT) とは、システムの仕様からテストケースを半自動生成するランダムテスト手法です。

PBT に対して、ふつうのテストを Example-Based Testing (EBT) と呼ぶこともあるようです。 EBT では具体例を連ねるのに対して、PBT ではもう少し抽象的にシステムの入力を指定することになります。

EBT の問題点

良いテストを設計し、メンテナンスするのは大変

ふつうのユニットテストでは、テスト作成者が見繕ったいくつかのテストケースについて、関数の出力やシステムの状態が期待したものになっているかどうかを確認します。 どのようなテストケースを作り、何をテストするかはテスト作成者に委ねられます。

ソフトウェアテストのベストプラクティス1を学び、テスト対象を深く理解し、テスト作成の経験を積めば、良いテストケースを見いだせるようになるでしょう。 それでも、十分網羅的なテストケースを作成し、バグの有無を隈無く確認するのは容易ではありません

例えば、以下のテストは Ruby の Array.fill のテストです。

  def test_fill_0
    assert_equal([-1, -1, -1, -1, -1, -1], [0, 1, 2, 3, 4, 5].fill(-1))
    assert_equal([0, 1, 2, -1, -1, -1], [0, 1, 2, 3, 4, 5].fill(-1, 3))
    assert_equal([0, 1, 2, -1, -1, 5], [0, 1, 2, 3, 4, 5].fill(-1, 3, 2))
    assert_equal([0, 1, 2, -1, -1, -1, -1, -1], [0, 1, 2, 3, 4, 5].fill(-1, 3, 5))
    assert_equal([0, 1, -1, -1, 4, 5], [0, 1, 2, 3, 4, 5].fill(-1, 2, 2))
    assert_equal([0, 1, -1, -1, -1, -1, -1], [0, 1, 2, 3, 4, 5].fill(-1, 2, 5))
    assert_equal([0, 1, 2, 3, -1, 5], [0, 1, 2, 3, 4, 5].fill(-1, -2, 1))
    assert_equal([0, 1, 2, 3, -1, -1, -1], [0, 1, 2, 3, 4, 5].fill(-1, -2, 3))
    assert_equal([0, 1, 2, -1, -1, 5], [0, 1, 2, 3, 4, 5].fill(-1, 3..4))
    assert_equal([0, 1, 2, -1, 4, 5], [0, 1, 2, 3, 4, 5].fill(-1, 3...4))
    assert_equal([0, 1, -1, -1, -1, 5], [0, 1, 2, 3, 4, 5].fill(-1, 2..-2))
    assert_equal([0, 1, -1, -1, 4, 5], [0, 1, 2, 3, 4, 5].fill(-1, 2...-2))
    assert_equal([10, 11, 12, 13, 14, 15], [0, 1, 2, 3, 4, 5].fill{|i| i+10})
    assert_equal([0, 1, 2, 13, 14, 15], [0, 1, 2, 3, 4, 5].fill(3){|i| i+10})
    assert_equal([0, 1, 2, 13, 14, 5], [0, 1, 2, 3, 4, 5].fill(3, 2){|i| i+10})
    assert_equal([0, 1, 2, 13, 14, 15, 16, 17], [0, 1, 2, 3, 4, 5].fill(3, 5){|i| i+10})
    assert_equal([0, 1, 2, 13, 14, 5], [0, 1, 2, 3, 4, 5].fill(3..4){|i| i+10})
    assert_equal([0, 1, 2, 13, 4, 5], [0, 1, 2, 3, 4, 5].fill(3...4){|i| i+10})
    assert_equal([0, 1, 12, 13, 14, 5], [0, 1, 2, 3, 4, 5].fill(2..-2){|i| i+10})
    assert_equal([0, 1, 12, 13, 4, 5], [0, 1, 2, 3, 4, 5].fill(2...-2){|i| i+10})
    assert_equal([0, 1, 2, 13, 14, 15], [0, 1, 2, 3, 4, 5].fill(3..){|i| i+10})
    assert_equal([0, 1, 2, 13, 14, 15], [0, 1, 2, 3, 4, 5].fill(3...){|i| i+10})
  end
  • これらのテストは網羅的ですか?すなわち、バグがあればこのテストのいずれかが失敗しますか?
  • これらのテストは必要ですか?昔は必要だったけど今はもういらないテストや、あとから追加したテストと同じことをテストしているテストはありませんか?
  • なんか数字がたくさん並んでいますが、それぞれどういう意図で選ばれたものなのでしょうか?

以上のことを、このテストコードから判断するのは簡単ではないはずです。 これは、良いテストを作るコストが高いだけでなく、メンテナンスするコストも高いことを意味します。

システムの実装と、テストとが密結合する

システムの実装によっても適当なテストケースは変わりうるので、テストと実装が密結合してしまうのも EBT の問題です。

優れたプログラマは、インターフェースと実装とを分離するために、良い API を設計するでしょう。 にもかかわらず、カバレッジを高くするために実装を覗いてテストを足してしまうことはないでしょうか?

SQL Antipatterns というRDBおよびそのクエリに関するアンチパターンを集めた名著がありますが、この中で外交特権(diplomatic immunity)というアンチパターンが出てきます。これは、システム開発におけるベストプラクティスであるバージョン管理やテストなどの対象から、なぜか例外的にデータベースだけはしばしば外されてしまう(そして問題が発生する)ことを指摘したものです。

テストについても同様で、ソフトウェア開発のベストプラクティスに反すると問題が発生しがちです。 今回の場合、実装に依存したテストコードがあると、実装を変えるたびにテストを足したり、実装が変わって不要になったテストが残ってしまったりして、ソフトウェアの柔軟性をテストが阻害してしまいます。

PBT ではフレームワークがテストケースを半自動的に生成します。 半自動と言っているように、PBT ではテスト作成者はより抽象的な記述で生成するテストケースを指定します。 この結果、PBT でテストケースの選定をプログラムに任せることができます。退屈なことはプログラムにやらせましょう。

そうすると当然問題になるのは、人間が一つ一つ愛情を込めて丹精に選びぬいた温かみのあるテストケースと、プログラムが自動生成するテストケース、どちらがソフトウェアのバグを隈無く見つけられるのでしょうかということです。この話の続きは、このアドベントカレンダーで追って説明していきます。

PBT の三要素

冒頭では、Property-Based Testing (PBT) とは、システムの仕様からテストケースを半自動生成するランダムテスト手法だと定義しましたが、次の三つを記述するための DSL を定義するテストフレームワークを、Property-Based Testing と呼ぶと考えてもいいかもしれません。

  • 入力を生成する Generator
  • テスト対象の満たすべき性質を述べた Property
  • テストに失敗したケースを人に理解しやすく変形する Shrinking

次回の記事では、Generator, Property, Shrinking についてより詳しく説明し、PBT を使ったテストがどのようなものになるかをもう少し具体的に説明する予定です。

==> Day3: Generator, Property, Shrinking

  1. テスト手法を網羅的に勉強したい初学者には、知識ゼロから学ぶソフトウェアテストがおすすめです