“ダミー値”を自動で作成する型クラス

# はじめに

単体テストを行う際には、テスト対象に渡したり、あるいはモックの引数や返り値などととして利用したりする値やインスタンスが必要となる[^instance]。このような単体テストでのみ利用する値をこの記事では**ダミー値**と呼ぶが、従来はテストを作成するプログラマーが手作業で作成しており、巨大な依存を持つユニットのテストは、ダミー値の作成だけで大きな労力が必要であった。この記事では任意の型`A`のダミー値を自動で作成する型クラス`TestObject[S, A]`について述べる。この型クラス`TestObject[S, A]`のインスタンスは常に同じダミー値を生成することもできる一方で、**Stateモナド**を利用して異なる値を生成することも可能である。また、この`TestObject[S, A]`を利用してどのようにテストを書くかについても言及する。この記事ではまず、モチベーションを説明するための例題として典型的なデータ構造を定義し、それをJSONへ変換するコンバーターにを考える。このコンバーターへの単体テストを作るに際して著者らが提案する型クラス`TestObject`を利用する。
この記事を読み、疑問や改善点などを見つけた場合はコメントなどで気軽に教えて欲しい。また、この記事の成果は @ippei-takahashi との共同作業である。なお、記事内の完全なソースコードは下記のリポジトリにある。

- https://github.com/y-yu/test-object

[^instance]: この記事では今後、クラスなどのインスタンスと値を区別せずにどちらも値と表現する。

# データ構造のJSON変換

この記事では例として、次のようなデータ構造`User`をJSONへ変換するプログラムのテストについて考える。まず`User`を次のように定義する。

```scala:User.scala
import org.joda.time.DateTime

case class User(id: UserId, info: UserView)

case class UserId(value: String)

case class UserView(
  emailAddress: Seq[EmailAddress],
  sex: Sex,
  age: Age,
  createdAt: DateTime
)
```

```scala:EmailAddress.scala
case class EmailAddress(value: String)
```

```scala:Sex.scala
sealed abstract class Sex(val value: String)

case object Female extends Sex("female")

case object Male extends Sex("male")

case class Unknown(v: String) extends Sex(s"Unknown($v)")
```

```scala:Age.scala
case class Age(value: Int)
```

このようなデータ構造`User`をJSONへ変換するプログラム`UserJsonConverter`を次のように定義したとする。

```scala:UserJsonConverter.scala
class UserJsonConverter {
  def toJson(user: User): JsValue = {
    import UserJsonConverter._
    Json.toJson(user)
  }

  def prettyPrint(user: User): String =
    Json.prettyPrint(toJson(user))
}

object UserJsonConverter {
  implicit val datetimeFormat: Writes[DateTime] = JodaWrites.JodaDateTimeWrites
  implicit val userIdWrites: Writes[UserId] = Writes(id => JsString(id.value))
  implicit val userViewWrites: Writes[UserView] = (
      (__ \ "email_address").write[Seq[String]].contramap[Seq[EmailAddress]](_.map(_.value)) and
      (__ \ "sex").write[String].contramap[Sex](_.value) and
      (__ \ "age").write[Int].contramap[Age](_.value) and
      (__ \ "created_at").write[DateTime]
    )(unlift(UserView.unapply))
  implicit val userWrites: Writes[User] = Json.writes[User]
}
```

これは次のように利用する。

```scala:Main.scala
object Main {
  def main(args: Array[String]): Unit = {
    val user = User(
      id = UserId("2"),
      info = UserView(
        emailAddress = Seq(EmailAddress("a@example.com"), EmailAddress("b@example.com")),
        sex = Male,
        age = Age(20),
        createdAt = new DateTime("2018-03-15")
      )
    )

    val converter = new UserJsonConverter

    println(converter.prettyPrint(user))
  }
}
```

このプログラムは次のように動作する。

```console
$ sbt run
{
  "id" : "2",
  "info" : {
    "email_address" : [ "a@example.com", "b@example.com" ],
    "sex" : "male",
    "age" : 20,
    "created_at" : "2018-03-15T00:00:00.000+09:00"
  }
}
```

# `TestObject`の使い方

型クラス`TestObject`を解説する前に、まずは`TestObject`をどのように利用するかについて述べる。いま、`UserJsonConverter`への単体テストをいくつか定義したい。このとき、`UserConvereter`に渡す型`User`の値が必要である。このとき我々の`TestObject`を利用すると次のように書くことができる。

```scala:UserJsonConverterSpec.scala
class UserJsonConverterSpec extends WordSpec with MustMatchers {
  trait SetUp {
    val sut = new UserJsonConverter
  }
  
  "toJson" should {
    "return a constant JSON successfully" in new SetUp {
      val constantUser: User = ConstantTestObject[User]

      val expected: JsValue = Json.parse(
        """
          |{
          |  "id" : "string",
          |  "info" : {
          |    "email_address" : ["string", "string", "string"],
          |    "sex" : "female",
          |    "age" : 123,
          |    "created_at" : "2018-03-13T00:00:00.000+09:00"
          |  }
          |}
        """.stripMargin
      )

      val actual = sut.toJson(constantUser)

      actual must be(expected)
    }
  }
}
```

`ConstantTestObject`は`TestObject`から派生した型クラスであるが、このように`ConstantTestObject[User]`するだけで、適当な値を投入した型`User`の値`constantUser`を作成できる。プログラマーは`ConstantTestObject[User]`を直接定義してはいない。
また、異なるいくつかの値が必要な場合には、次のように書くことができる。

```scala:UserJsonConverterSpec.scala
"return some JSONs successfully" in new SetUp {
  val someUsers: Seq[User] = (for {
    u1 <- DeterministicTestObject[User]
    u2 <- DeterministicTestObject[User]
  } yield Seq(u1, u2)).apply(0)._2
 
  val expectedSeq: Seq[JsValue] = Seq(
    Json.parse(
      """
        |{
        |  "id" : "0",
        |  "info" : {
        |    "email_address" : ["1", "2", "3"],
        |    "sex" : "Unknown(4)",
        |    "age" : 5,
        |    "created_at" : "2018-03-19T00:00:00.000+09:00"
        |  }
        |}
      """.stripMargin
    ),
    Json.parse(
      """
        |{
        |  "id" : "7",
        |  "info" : {
        |    "email_address" : ["8", "9", "10"],
        |    "sex" : "male",
        |    "age" : 11,
        |    "created_at" : "2018-03-25T00:00:00.000+09:00"
        |  }
        |}
      """.stripMargin
    )
  )
 
  (someUsers zip expectedSeq) foreach {
    case (user, expected) =>
      val actual = sut.toJson(user)
      actual must be(expected)
  }
}
```

このようにダミー値を作ることができる`TestObject`の詳細について次から解説する。

# `TestObject[S, A]`

型クラス`TestObject[S, A]`は次のようなStateモナドのラッパーである。

```scala:TestObject.scala
trait TestObject[S, A] {
  def generate: State[S, A]
}
```

この`TestObject[S, A]`の型パラメータ`S`に入れる型を`Unit`としたものが`ConstantTestObject[A]`であり、`Int`としたものが`DeterministicTestObject[A]`である。

```scala:ConstantTestObject.scala
trait ConstantTestObject[A] extends TestObject[Unit, A]
```

```scala:DeterministicTestObject.scala
trait DeterministicTestObject[A] extends TestObject[Int, A]
```

## Stateモナド

具体的なインスタンスについて述べる前に、Stateモナドについて軽く説明する。Stateモナドとは次のように定義されたデータ構造である。状態の型`S`と結果の型`A`を用いて次のようになる。

```scala
abstract class State[S, A] { self =>
  def run(s: S): (S, A)

  def flatMap[B](f: A => State[S, B]): State[S, B] =
    new State[S, B] {
      def run(s: S): (S, B) = {
        val (n, a) = self.run(s)
        f(a).run(n)
      }
    }

  def map[B](f: A => B): State[S, B] =
    new State[S, B] {
      def run(s: S): (S, B) = {
        val (n, a) = self.run(s)
        (n, f(a))
      }
    }
}

object State {
  def apply[S, A](f: S => (S, A)): State[S, A] =
    new State[S, A] {
      def run(s: S): (S, A) = f(s)
    }
  
  def get[S]: State[S, S] =
    new State[S, S] {
      def run(s: S): (S, S) = (s, s)
    }
}
```

Stateモナドはこのように状態を更新しながら、最終的に関数`run`に初期値を与えると型`A`の値を生成する。また、ユーティリティーとして関数`get`を定義する。この関数はStateモナド`State[S, A]`から現在の状態の値を入手することができる。

## `ConstantTestObject[A]`のインスタンス

それでは`ConstantTestObject[A]`のインスタンスについて述べる。まずは型パラメーター`A`を`Int`や`String`などとした自明なインスタンスは次のように作る。

```scala:ConstantTestObject.scala
type UnitState[A] = State[Unit, A]

private def state[A](a: A): UnitState[A] =
  State(_ => ((), a))
 
implicit val testString: ConstantTestObject[String] = new ConstantTestObject[String] {
  def generate: UnitState[String] = state("string")
}
 
implicit val testInt: ConstantTestObject[Int] = new ConstantTestObject[Int] {
  def generate: UnitState[Int] = state(123)
}

implicit def testOption[A: ConstantTestObject]: ConstantTestObject[Option[A]] = new ConstantTestObject[Option[A]] {
  def generate: UnitState[Option[A]] = {
    state(Some(implicitly[ConstantTestObject[A]].generate(())._2))
  }
}
```

プライベート関数`state`はStateモナドを作成するユーティリティ関数である。Stateモナドの状態の型は`Unit`であり、この値を利用することは意味がないので捨てている。このように他にも`Float`や`Double`といった自明な型について、対応するStateモナドの作り方を手で定義する。
次に上述した`User`のようなプログラマが定義した型のインスタンスについて考える。まず、次のような`AbstractTestObject[S]`を考える。

```scala:AbstractTestObject.scala
trait AbstractTestObject[S] {
  trait TestHList[L <: HList] {
    def generate: State[S, L]
  }

  trait TestCoproduct[C <: Coproduct] {
    def generate: State[S, C]
  }
}
```

`AbstractTestObject`はStateモナドの状態の型を固定して、`HList`や`Coproduct`を結果の型として持つようなStateモナドを作成する型クラスを定義している。これを利用することで`User`を自動生成する`ConstantTestObject[User]`のインスタンスを作成できるが、まずは`HList`と`Coproduct`について軽く解説する。

### `HList`と`Coproduct`とマクロ

`HList`は次のような定義となっている[^shapeless]。

```scala:HList.scala
sealed trait HList
case class ::[+A, +B <: HList](h: A, t: B) extends HList {
  def ::[C](x: C): C :: A :: B = ::(x, this)
}
sealed trait HNil extends HList {
  def ::[A](x: A): A :: HNil = ::(x, this)
}
```

[^shapeless]: 実装では[shapeless](https://github.com/milessabin/shapeless)の実装を用いたが、手で実装してもよい。

通常のリストは`List[A]`のように型`A`の値が任意の数あるようなデータ構造であるが、一方で`HList`は型レベルで値の型とインデックスを保存しているため、たとえば次のようにいくつかの型を同時に格納するようなリストを表現できる。

```scala
1 :: "string" :: 1.0 :: HNil
```

これの型は`Int :: String :: Float :: HNil`となる。つまり先頭に`Int`型の値があり、次に`String`型の値があり、最後に`Float`型の値があるということを表現できる。このように型とその型の値が出現するインデックスを型レベルで持つデータ構造を`HList`と呼ぶ。さて、上記の`User`型について考える。`User`は次のようなデータ構造である。

```scala:User.scala
case class User(id: UserId, info: UserView)
```

このデータ構造は先頭に`UserId`型の値をもち、次に`UserView`型の値を持つデータ構造とみなすこともできる。このように、ユーザーが定義した型は最終的にプリミティブな型をある順序で並べた構造としてHListで表現できそうである。ところが、型`UserView`に含まれている型`Sex`は次のようになっている。

```scala:Sex.scala
sealed abstract class Sex(val value: String)

case object Female extends Sex("female")

case object Male extends Sex("male")

case class Unknown(v: String) extends Sex(s"Unknown($v)")
```

このように型`Sex`は`Female`か`Male`か`Unknown`のいずれかであって、これを全て並べたものではない。そこでこのようなデータ構造を`HList`へ変換することはできない。このようなデータ構造は`Coproduct`へ変換できる。`Coproduct`は次のようなデータ構造である[^shapeless]。

```scala:Coproduct.scala
sealed trait Coproduct

sealed trait :+:[+H, +T <: Coproduct] extends Coproduct

case class Inl[+H, +T <: Coproduct](head : H) extends :+:[H, T]

case class Inr[+H, +T <: Coproduct](tail : T) extends :+:[H, T]

sealed trait CNil extends Coproduct
```

簡単に言えば`Either`であるが、右側（`Inr`）が取る型が`Coproduct`のサブタイプであることを強制している[^copro_vs_either]。これを利用することで、次のように先程の`Sex`を対応する`Coproduct`へ変換することができる。

[^copro_vs_either]: この定義とは異なり、`Coproduct`は次のような高階な`Either`を指すこともある。

    ```scala
      case class Coproduct[F[_], G[_], A](value: Either[F[A], G[A]])
    ```

```scala
Female :+: Male :+: Unknown :+: CNil
```

`HList`と`Coproduct`を利用して、型`User`をプリミティブな型だけで表現すると次のようになる。

```scala
(String :: HNil) :: ((Seq[String :: HNil] :: HNil) :: (Female :+: Male :+: Unknown :+: CNil) :: (Int :: HNil) :: DateTime :: HNil) :: HNil
```

さて、このように`HList`と`Coproduct`を利用することで、ユーザーが定義した任意の型をプリミティブな型のあつまりで表現できることが分った。また[shapeless](https://github.com/milessabin/shapeless)のマクロを利用することで、ユーザーが定義した任意の型から`HList`と`Coproduct`へ変換することができる。

### `ConstantTestObject[HList]`と`ConstantTestObject[Coproduct]`

あとは`HList`と`Coproduct`と、さきほど定義したプリミティブな型の値を作成するインスタンスからユーザーが定義した型の値のインスタンスを次のように作成できる。まずは`HList`のインスタンスを次のように作成する。

```scala:ConstantTestObject.scala
implicit val testHNil: TestHList[HNil] = new TestHList[HNil] {
  def generate: UnitState[HNil] = state(HNil)
}

implicit def testHCons[H, T <: HList](
  implicit
  head: ConstantTestObject[H],
  tail: TestHList[T]
): TestHList[H :: T] = new TestHList[H :: T] {
  def generate: UnitState[H :: T] =
    for {
      h <- head.generate
      t <- tail.generate
    } yield h :: t
}

implicit def testHList[A, L <: HList](
  implicit
  gen: Generic.Aux[A, L],
  testHList: Lazy[TestHList[L]]
): ConstantTestObject[A] = new ConstantTestObject[A] {
  def generate: UnitState[A] =
    testHList.value.generate.map(gen.from)
}
```

まず、`HNil`の場合はそのまま`HNil`を返す自明な実装となり、一方で`H :: T`という`HList`の場合は次のような順序で生成する。

1. リストの先頭の値を生成する
    - 先頭の型`H`については`ConstantTestObject[H]`のインスタンスを要求するので、型`H`の値を生成できる
2. リストの先頭以外の値を生成する
    - `TestHList[T]`のインスタンスを要求するので、型`T <: HList`の値の列を生成できる

そして最後にshapelessから提供された型クラス`Generic.Aux`を利用して、任意の型`A`に対応する`HList`である`L`へ変換し、プリミティブな型のインスタンスから最終的な型`A`の値を組み立てることができる。
これと同様に`Coproduct`を次のように組み立てる。

```scala:ConstantTestObject.scala
implicit val testCNil: TestCoproduct[CNil] = new TestCoproduct[CNil] {
  def generate: UnitState[CNil] = throw new RuntimeException()
}
 
implicit def testCCons[H, T <: Coproduct](
  implicit
  inl: ConstantTestObject[H],
  inr: TestCoproduct[T]
): TestCoproduct[H :+: T] = new TestCoproduct[H :+: T] {
  def generate: UnitState[H :+: T] = inl.generate.map(Inl(_))
}

implicit def testCoproduct[A, C <: Coproduct](
  implicit
  gen: Generic.Aux[A, C],
  testCoproduct: Lazy[TestCoproduct[C]]
): ConstantTestObject[A] = new ConstantTestObject[A] {
  def generate: UnitState[A] =
    testCoproduct.value.generate.map(gen.from)
}
```

まず`CNil`はインスタンス化ができないので、型をあわせるために適当な例外を例外を投げるようにする。ただこのインスタンス`testCNil`が利用されることはなく、インスタンス`testCCons`では常に左側の値を用いることとしている。このようにして`Coproduct`の生成ルールを与え、最後に`Generic.Aux`を利用して`HList`と同様に任意の型`A`の値を作成することができる。

## `DeterministicTestObject[A]`のインスタンス

さて、`DeterministicTestObject[A]`は型`A`の値を返すが、それはStateモナドの状態（`Int`）によって生成される。`ConstantTestObject`は状態を無視していたが、こちらは状態を次のように利用する。

```scala:DeterministicTestObject.scala
implicit val testString: DeterministicTestObject[String] = new DeterministicTestObject[String] {
  def generate: IntState[String] = State(s => (s + 1, s.toString))
}
 
implicit val testInt: DeterministicTestObject[Int] = new DeterministicTestObject[Int] {
  def generate: IntState[Int] = State(s => (s + 1, s))
}

implicit def testOption[A: DeterministicTestObject]: DeterministicTestObject[Option[A]] = new DeterministicTestObject[Option[A]] {
  def generate: IntState[Option[A]] =
    for {
      a <- implicitly[DeterministicTestObject[A]].generate
      s <- State.get
    } yield
      if (s % 2 == 0)
        Some(a)
      else
        None
}
```

このようにプリミティブな型については`Int`型の値から雑に生成し、そして状態に`1`を加算するという更新を行う。また、前述の`ConstantTestObject`と同様に`HList`と`Coproduct`を利用して次のように任意の型へ拡張する。また`Option`の場合は状態によって`Some`となったり、または`None`となるようにしてある。

```scala:DeterministicTestObject.scala
implicit val testHNil: TestHList[HNil] = new TestHList[HNil] {
  def generate: IntState[HNil] = State(s => (s, HNil))
}
 
implicit def testHCons[H, T <: HList](
  implicit
  head: DeterministicTestObject[H],
  tail: TestHList[T]
): TestHList[H :: T] = new TestHList[H :: T] {
  def generate: IntState[H :: T] = {
    for {
      h <- head.generate
      t <- tail.generate
    } yield h :: t
  }
}
 
implicit def testHList[A, L <: HList](
  implicit
  gen: Generic.Aux[A, L],
  testHList: Lazy[TestHList[L]]
): DeterministicTestObject[A] = new DeterministicTestObject[A] {
  def generate: IntState[A] =
    testHList.value.generate.map(gen.from)
}
```

`HList`について述べることは、`HNil`の値を生成する場合は状態を更新しないことである。`HNil`の生成は最終的に`testHList`インスタンスでユーザーの定義した型へ戻す際に失われるため、状態を更新しない方が自然な結果が得られる。
`Coproduct`については次のようになる。

```scala:DeterministicTestObject.scala
implicit val testCNil: TestCoproduct[CNil] = new TestCoproduct[CNil] {
  def generate: IntState[CNil] = throw new RuntimeException
}
 
implicit def testCCons[H, T <: Coproduct](
  implicit
  inl: DeterministicTestObject[H],
  inr: TestCoproduct[T]
): TestCoproduct[H :+: T] = new TestCoproduct[H :+: T] {
  def generate: IntState[H :+: T] = {
    def or(l: H, rOpt: Option[T], s0: Int, s1: Int, s2: Int): IntState[H :+: T] = rOpt match {
      case Some(r) =>
        if (s2 % 2 == 0)
          State(_ => (s1, Inl(l)))
        else
          State(_ => (s0 + s2 - s1, Inr(r)))
      case None =>
        State(_ => (s1, Inl(l)))
    }
 
    for {
      s0 <- State.get
      l <- inl.generate
      s1 <- State.get
      rOpt <- try {
        inr.generate.map(Some(_))
      } catch {
        case e: Throwable =>
          State((s: Int) => (s, None))
      }
      s2 <- State.get
      out <- or(l, rOpt, s0, s1, s2)
    } yield out
  }
}
 
implicit def testCoproduct[A, C <: Coproduct](
  implicit
  gen: Generic.Aux[A, C],
  testCoproduct: Lazy[TestCoproduct[C]]
): DeterministicTestObject[A] = new DeterministicTestObject[A] {
  def generate: IntState[A] =
    testCoproduct.value.generate.map(gen.from)
}
```

`H :+: T`のケースが`ConstantTestObject`と比べて複雑となっている。これは次のような処理を行っているためである。

1. 左側の型`H`の値を生成する。これは確実に成功する
2. 右側の型`T`の値を生成する
    - しかし、型`T`が`CNil`である場合は生成に失敗する。失敗した場合にそなえて、成功か失敗かの`Option`値を返すようにしている
3. 次のように条件分岐をする
    - もし右側の値を作成できなかった場合、左側の値を利用する。このとき次の状態は左側の作成によって更新された値を利用する
    - もし右側の値を作成できた場合、次のように分岐する
        - 右側の値を作成した後の状態が**偶数**であるとき、左側の値を利用する。このとき次の状態は左側の作成によって更新された値を利用する
        - 右側の値を作成した後の状態が**奇数**であるとき、右側の値を利用する。このとき次の状態は最終的な状態から左側の値を作成によって発生した更新を削除する

このようにして最終的な値を得ることができる。

# まとめ

型クラスとマクロを利用することで、テストなどで必要となるダミー値を簡単に作成することができた。ダミー値の作成に必要な時間を短縮してテストをより効率的に書けるのではないかと思われる。