ScalaCheckでプロパティベーステストしてみる(2)

# 概要

[前回の記事](https://qiita.com/tobita_yoshiki/items/81581062daa252800652)では、最大値を求める関数java.lang.Math.maxをプロパティベーステストしました。

今回の記事では、 文字列を切り詰める関数`truncate`, トークン化する関数`tokenize`, 特定の部分文字列が含まれているかどうか判定する関数`contains`について、 プロパティベーステストします。

## テスト対象のコード

```scala
object StringUtils:
  def truncate(s: String, n: Int): String =
    if(s.length <= n)
      s
    else
      s.substring(0, n) + "..."

  def tokenize(s: String, delim: Char): Array[String] =
    s.split(delim)

  def contains(s: String, subString: String): Boolean = s.contains(subString)
```

## truncateをテストする

以下のテストコードは`truncate`関数をテストするものです。
forAllに渡している引数`s`と`n`はScalaCheckによって生成されるテストパラメータで、
ランダムに生成されます。

```scala
  property("truncate") =
    Prop.forAll { (s: String, n: Int) =>
      val t = StringUtils.truncate(s, n)
      (s.length <= n && t == s) ||
      (s.length > n && t == s.take(n) + "...")
    }
```

実行すると以下の内容が標準出力されました。

```
! StringUtils.truncate: Exception raised on property evaluation.
> ARG_0: ""
> ARG_1: -1
> ARG_0_ORIGINAL: "㾌〤鵛"
> ARG_1_ORIGINAL: -1657552462
> Exception: java.lang.StringIndexOutOfBoundsException: Range [0, -1) out o
  f bounds for length 0
```

これは`n`が負数のときに`s.substring(0, n)`によって引き起こされる例外です。

事例ベースのテストですと、`n`に負数を渡すことを思いつかないかこともあるかと思いますので、
このケースが見つかったのはプロパティベーステストならではですね🤔

以下、例外のスローに対応したテストコードです:

```scala:修正版
property("truncate") =
  Prop.forAll { (s: String, n: Int) =>
    lazy val t = StringUtils.truncate(s, n)
    if (n < 0)
      Prop.throws(
        classOf[StringIndexOutOfBoundsException]
      ) { t }
    else
      (s.length <= n && t == s) ||
      (s.length > n && t == s.take(n)+"...")
  }
```

## tokenizeをテストする

```scala
  property("tokenize") =
    Prop.forAll(listOf(alphaStr), numChar) {
      (ts, d) =>
        val str = ts.mkString(d.toString)
        StringUtils.tokenize(str, d).toList == ts
    }
```

`alphaStr`および`numChar`はScalaCheck組み込みのジェネレータです。

`alphaStr`はアルファベットでのみ構成される文字列を生成するもので、
`numChar`はChar型の数値を生成するものです。

#### alphaStrとnumCharで値を生成してみる

```scala:test.sc
//> using dep "org.scalacheck::scalacheck::1.18.0"

(1 to 10).foreach { _ =>
  println(org.scalacheck.Gen.alphaStr.sample)
}

(1 to 10).foreach { _ =>
  println(org.scalacheck.Gen.numChar.sample)
}
```

```txt:値の生成
$ scala-cli test.sc
Warning: Only java properties are supported in JAVA_OPTS and JDK_JAVA_OPTIONS environment variables. Other options are ignored: -Xmx4G
Compiling project (Scala 3.4.2, JVM (22))
Compiled project (Scala 3.4.2, JVM (22))
Some(JGVyzUpfEkAVkoBUYXUMTvfvliRJtVKWTjNHBOOQlYqxFNanRbqzoAbMnIqKYIfMkNhpRmTrxPBVlYtzFCOBBysyOcvqnnCei)
Some(DbKYUOPRuDHYMaSCjiorNBUkFBUG)
Some(ueSmIuDApxejpiuwHxIARjClckccHNKvUtbRGMq)
Some(DKDtjUIYdggOCoLTesXDwuTItRaIiGbZaaAHKfMgN)
Some(yYfmifzMdwXOqrKstvnNAPBlpJd)
Some(kGhMoxmpqS)
Some(zULKcHkHbCzMbYaczhxBtFpkNTHFUxIwyeSwn)
Some(IQAzUYPMpWejNJnOolftHJiBswFHjVApjfk)
Some(pQOTRiBSmVsJoOvUyHYWtXKwlSzDCLntVIiYfNdYpcmmDzFCEbRSGnoGymtEoGhF)
Some(GlgptWSALMaTJjszcQXnVtAadHYVRUBHzdNvYUhhcbuqlOIaIJfmesbkdoSHIGQAdJeQalwkqvWXsAZCVtreUAdwkWFqNRxiZl)
Some(7)
Some(4)
Some(1)
Some(5)
Some(3)
Some(1)
Some(7)
Some(7)
Some(0)
Some(4)
```

## containsをテストする

```scala
  property("contains") = Prop.forAll {
    (s1: String, s2: String, s3: String) =>
      StringUtils.contains(s1+s2+s3, s2)
  }
```

単にStringを指定した場合は任意の文字列がテストに使用されます。