概要
ScalaCheckはScalaのプロパティベーステストライブラリで、テストデータの自動生成にArbitraryというクラスを使います。
しかし、複雑な構造を持つオブジェクトに対してArbitraryを手動で定義するのは面倒です。
この記事では、Scala 3のコンパイル時リフレクションを活用して、任意のオブジェクトに対するArbitrary
を自動生成する方法を紹介します。
ScalaCheckとArbitrary
ScalaCheckでは、Arbitrary[T]
を提供することで、型T
の値をランダムに生成できます。
例えば、Arbitrary[Int]
はランダムな整数を生成し、Arbitrary[String]
はランダムな文字列を生成します。
自動導出の仕組み
Scala 3のコンパイル時リフレクション機能を使うと、ケースクラスや列挙型(enum)などの構造を解析し、それに基づいてArbitrary
インスタンスを自動生成できます。
import org.scalacheck.Arbitrary
import org.scalacheck.Gen
import scala.compiletime.{erasedValue, error, summonFrom, summonInline}
import scala.deriving.*
trait LowPriorityScalaCheckDerive {
given arbitraryProduct[T](using mirror: Mirror.ProductOf[T], arbitrary: Arbitrary[mirror.MirroredElemTypes]): Arbitrary[T] = Arbitrary {
arbitrary.arbitrary.map { (v: mirror.MirroredElemTypes) =>
mirror.fromProduct(v)
}
}
}
object ScalaCheckDerive extends LowPriorityScalaCheckDerive {
inline def summonGens[T, Elems <: Tuple]: List[Gen[T]] =
inline erasedValue[Elems] match
case _: (elem *: elems) =>
summonInline[Arbitrary[elem]].arbitrary.asInstanceOf[Gen[T]] :: summonGens[T, elems]
case _: EmptyTuple =>
Nil
inline given arbitrarySum[A](using mirror: Mirror.SumOf[A]): Arbitrary[A] =
Arbitrary(Gen.sequence[List[A], A](summonGens[A, mirror.MirroredElemTypes]).flatMap(Gen.oneOf))
given arbitraryCaseObject[T](using v: ValueOf[T]): Arbitrary[T] =
Arbitrary(Gen.const(v.value))
}
object ClientApp extends App {
import ScalaCheckDerive.given
sealed trait OS
case object Windows extends OS
case object MacOS extends OS
case class UnknownOS(name: String) extends OS
case class Device(name: String, os: OS)
println(Arbitrary.arbitrary[Device].sample) // 例: Some(Device(쇭띋⎗䗜燴䭛淌舂렴銴쟳熬䞆嶱ⶫ녫簭勺뇘ዮ琿鵟胵㏻鮩箛먁İ᱙岢옯ᰐ䌐ꦞ,Windows))
}
解説
-
LowPriorityScalaCheckDerive
:- 優先度の低い
given
を定義するためのトレイトです。 -
arbitraryProduct
は、Mirror.ProductOf[T]
(case classのミラー)とArbitrary[mirror.MirroredElemTypes]
(case classの各フィールドの型のArbitrary
)が与えられた場合に、Arbitrary[T]
を生成します。
- 優先度の低い
-
ScalaCheckDerive
:-
LowPriorityScalaCheckDerive
を継承し、追加のgiven
を定義します。 -
summonGens
は、直和型(sealed trait)の各ケースクラス/オブジェクトに対応するGen[T]
のリストを生成します。 -
arbitrarySum
は、Mirror.SumOf[A]
(直和型のミラー)が与えられた場合に、Arbitrary[A]
を生成します。 -
arbitraryCaseObject
は、case objectに対応するArbitrary[T]
を生成します。
-
-
ClientApp
:-
ScalaCheckDerive.given
をインポートすることで、ScalaCheckDerive
で定義されたgiven
を利用できるようにします。 -
Arbitrary.arbitrary[Device]
を呼び出すと、Device
型の値をランダムに生成できます。
-
注意点
- このコードは、case class、case object、sealed trait、enumなどの基本的な型に対しては自動的に
Arbitrary
を導出できますが、より複雑な型や再帰的な型に対しては対応できない場合があります。 - 再帰的な型や複雑な依存関係を持つ型に対しては、手動で
Arbitrary
インスタンスを定義する必要があります。
まとめ
Scala 3のコンパイル時リフレクション機能を活用することで、多くのケースでArbitrary
インスタンスを手動で定義する手間を省くことができます。これにより、プロパティベーステストの作成がより簡単になります。
参考