0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

Scala 3でScalaCheckのArbitraryを自動導出:ケースクラス、列挙型、直和型に対応

Last updated at Posted at 2024-06-29

概要

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))
}

解説

  1. LowPriorityScalaCheckDerive:
    • 優先度の低いgivenを定義するためのトレイトです。
    • arbitraryProductは、Mirror.ProductOf[T](case classのミラー)とArbitrary[mirror.MirroredElemTypes](case classの各フィールドの型のArbitrary)が与えられた場合に、Arbitrary[T]を生成します。
  2. ScalaCheckDerive:
    • LowPriorityScalaCheckDeriveを継承し、追加のgivenを定義します。
    • summonGensは、直和型(sealed trait)の各ケースクラス/オブジェクトに対応するGen[T]のリストを生成します。
    • arbitrarySumは、Mirror.SumOf[A](直和型のミラー)が与えられた場合に、Arbitrary[A]を生成します。
    • arbitraryCaseObjectは、case objectに対応するArbitrary[T]を生成します。
  3. ClientApp:
    • ScalaCheckDerive.givenをインポートすることで、ScalaCheckDeriveで定義されたgivenを利用できるようにします。
    • Arbitrary.arbitrary[Device]を呼び出すと、Device型の値をランダムに生成できます。

注意点

  • このコードは、case class、case object、sealed trait、enumなどの基本的な型に対しては自動的にArbitraryを導出できますが、より複雑な型や再帰的な型に対しては対応できない場合があります。
  • 再帰的な型や複雑な依存関係を持つ型に対しては、手動でArbitraryインスタンスを定義する必要があります。

まとめ

Scala 3のコンパイル時リフレクション機能を活用することで、多くのケースでArbitraryインスタンスを手動で定義する手間を省くことができます。これにより、プロパティベーステストの作成がより簡単になります。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?