1. 序章

ScalaCheck は、Haskellライブラリ QuickCheck に触発された、ステートフルおよびステートレスのプロパティベースのテストを自動化するライブラリです。 このライブラリを使用すると、エッジケースのシナリオを確認しながら、ボイラープレートコードなしでランダム入力を使用してテストを作成できます。

2. インストール

ScalaCheckは、sbtビルドファイルに依存関係として追加するだけで使用できます。

libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.14.1" % "test"

3. ステートレステスト:プロパティ

Propertiesクラスには、ステートレスプロパティテストの機能が含まれています。 具体的には、チェックに便利なメソッドを備えたプロパティのコレクションを保持します。

簡単なチェックの例を見てみましょう。

case class Simple(str: String) {
  def length: Int = str.length
}

object PropertiesUnitTest extends Properties("Simple") {
  property("length") = forAll { (str: String) =>
    Simple(str).length >= 0
  }
}

実行すると、コンソール出力が表示されます。

+ Simple.length: OK, passed 100 tests.

コンソール出力で述べたように、テストは100の異なる入力の組み合わせに対してチェックされました。 実際、必要な最小成功テストの数、並列実行のワーカーの数、およびその他のプロパティを変更できます。

構成機能を説明するために、ここでは minimumSuccessfulTests を変更し、onPropEvalのコールバック関数を設定します。

override def overrideParameters(p: Parameters): Parameters = {
  p.withMinSuccessfulTests(50)
    .withTestCallback(new TestCallback {
      override def onPropEval(name: String, threadIdx: Int, succeeded: Int, discarded: Int): Unit = {
        println(s"Evaluating prop with name: $name")
    }
  })
}

onPropEval コールバックを追加したため、コンソール出力は次のように変更されます。

...
Evaluating prop with name: CustomParameters.length
Evaluating prop with name: CustomParameters.length
...

4. ステートフルテスト:コマンド

それどころか、コマンド特性により、入力とその遷移を制御できます。確かに、可能な状態とその遷移を指定する必要があるため、このアプローチはステートマシンのように見えます。

信号機のライフサイクルをチェックするテストを作成しましょう。

まず、StateモデルとSystemUnderTestモデルを作成する必要があります。

sealed trait TrafficLightColorT {
  def color: String
}
case class TrafficLight(uuid: UUID, color: TrafficLightColorT)
case class SystemUnderTest(refId: UUID, date: Long, trafficLight: TrafficLight)

テストコンテキストモデルを作成したので、次のテストに進むことができます。

object CommandsUnitTest extends Commands {
  override type State = TrafficLight
  override type Sut = SystemUnderTest

// initialization and precondition checks omitted for brevity

override def genInitialState: Gen[TrafficLight] = {
  for (
    trafficLightColor <- Gen.oneOf(model.Red, model.Yellow, model.Green)
  ) yield TrafficLight(UUID.randomUUID(), trafficLightColor)
}

override def genCommand(state: TrafficLight): Gen[Command] = {
  state.color match {
    case model.Green => TransitionToYellow(state)
    case model.Yellow => TransitionToRed(state)
    case model.Red => TransitionToGreen(state)
    case _ => throw new RuntimeException("Traffic lights have only green, yellow and red color.")
  }
}

case class TransitionToGreen(trafficLight: TrafficLight) extends Command {
  override type Result = Boolean

  override def run(sut: SystemUnderTest): Boolean = {
    println("going green")
    true
  }

  override def nextState(state: TrafficLight): TrafficLight = state.copy(color = model.Green)

  override def preCondition(state: TrafficLight): Boolean = state.color == model.Red

  override def postCondition(state: TrafficLight, result: Try[Boolean]): Prop = result == Success(true)
}

// Yellow and Red transitions are identical except from the color.

5. ジェネレーター

現時点では、ScalaCheckがテストケースに繰り返しフィードするために大量のランダムデータを生成することは明らかです。 提供されているジェネレーターは非常に便利ですが、多くの場合、十分ではありません。 生成された値をさらに制御する必要がある場合があります。

具体的には、 Gen オブジェクトには、生成された値を操作する機能を提供する多くのヘルパー関数があります。 次のセクションでは、さまざまなジェネレーターの例を示します。

5.1. 選択

Gen.choose は、包括的範囲( num1 num2 )からランダムな数値を選択します。

private val choiceGen = Gen.choose(-10, 10)
property("choiceGen") = forAll(choiceGen) { num => num.abs <= 10 }

5.2. ピック

Gen.pick は、値のリストから指定された数の要素をランダムに選択します。 予想どおり、生成された値は反復可能です。

property("randomPick") = forAll(Gen.pick(2, Seq(1, 2, 3, 4, 5))) { seq => seq.sum > 0 }

5.3. oneOf

Gen.oneOf は、指定されたリストからランダムな要素を選択します。

private val oneOfGen = Gen.oneOf(Seq(1, 2, -10, 40))
property("oneOf") = forAll(oneOfGen) { num => num.abs >= 0 }

5.4. シーケンス

Gen.sequence は、名前が示すようにシーケンスを生成します。 シーケンス要素は、シーケンス引数を構成するジェネレーターによって作成されます。 上ですでに作成したシーケンスを使用する例を見てみましょう。

property("sequence") = forAll(Gen.sequence(Seq(choiceGen, oneOfGen))) { foo => foo.size >= 0 }

5.5. 頻度

Gen.frequency は、加重ランダム分布を作成します。 この例では、リンゴはバナナよりも希少であり、バナナはキウイよりも希少です。

Gen.frequency((2, "apples"), (4, "bananas"), (6, "kiwis"))

6. 結論

このチュートリアルでは、プロパティベースのテストのソリューションとしてScalaCheckを紹介しました。 ScalaCheckを使用して作成されたテストケースは、冗長性が低く、エッジケースの失敗に対する脆弱性が低いように見えます。

いつものように、上記の例のコードはGitHubから入手できます。