ScalaTestでScaledTimeSpansをうまく使うとテスト環境毎に係数をかけて実行時間を待ってくれる

タイトルが全てなのですが、具体的にどうやるかも書いておきます。

ドキュメントとしては、 http://www.scalatest.org/user_guide/using_the_runner#scalingTimeSpans の、 *Specifying a span scale factor* ってあたりに書いてあります。引用しておきます。

> If you specify a integer or floating point span scale factor with -F, trait ScaledTimeSpans trait will return the specified value from its implementation of spanScaleFactor. This allows you to tune the "patience" of a run (how long to wait for asynchronous operations) from the command line. For more information, see the documentation for trait ScaledTimeSpans.

なお、以下に引用しているソースコードは、3.1.xですが、実行自体は、ScalaTest 3.0.5で試してます。

# Example Code

こんな感じで書いてみました。

```scala
package example

import org.scalatest._
import org.scalatest.concurrent._

import scala.concurrent._

class HelloSpec extends FlatSpec with Matchers with ScalaFutures {
  import ExecutionContext.Implicits.global
  "The Hello object" should "say hello" in {
    Future {
      Thread.sleep(1000L);
      println("hoge");
    }.futureValue
    Hello.greeting shouldEqual "hello"
  }
}
```

# run test

sbtでtestを実行すると、以下のようなエラーになります。

```
[info] - should say hello *** FAILED ***
[info]   A timeout occurred waiting for a future to complete. Queried 10 times, sleeping 15 milliseconds between each query. (HelloSpec.scala:14)
```

# 分析

これは、

```scala
    }.futureValue
```

のところでエラーになっています。

といいますのも、Futureの中で1秒スリープしているのですが、これが待ち切れずにtimeoutしています。

このFutureをどれだけ待つかってあたりについては、

```scala
class HelloSpec extends FlatSpec with Matchers with ScalaFutures {
```

でMixinしている [ScalaFutures](https://github.com/scalatest/scalatest/blob/3.1.x/scalatest/src/main/scala/org/scalatest/concurrent/ScalaFutures.scala) をたどっていくと見えてくるのですが、最終的には、AbstractPatienceConfigurationで宣言されているcase classの、 [PatienceConfig](https://github.com/scalatest/scalatest/blob/3.1.x/scalatest/src/main/scala/org/scalatest/concurrent/AbstractPatienceConfiguration.scala#L91) が適用されています。

ここで設定されている、

```scala
timeout: Span = scaled(Span(150, Millis)), interval: Span = scaled(Span(15, Millis))
```

っていうのが適用されているため、Futureの中で実施している `Thread.sleep` を待ち切れずにエラーとなっています。

ここで設定されているtimeoutするまでintervalあけてリトライしていく処理になっており、15ミリ秒間をあけつつ、10回リトライして全部で150ミリ秒待ったけど、終わらなかったからエラーってロジックになっています。[^1]

[^1]: https://github.com/scalatest/scalatest/blob/3.1.x/scalatest/src/main/scala/org/scalatest/concurrent/Futures.scala#L533-L547

# 係数をかける

ScalaTestの[ドキュメント](http://www.scalatest.org/user_guide/using_the_runner)に、

| argument | description | example |
|:--|:--|:--|
| -F &lt;span scale factor&gt; | a factor by which to scale time spans<br>(Note: only one -F is allowed) | -F 10 or -F 2.5 |

とあり、こちらを実行時に指定すると、係数をかけることができます。

sbtで実行する際は、以下のどちらかの方法で実行できます。ここでは、10を係数としてかけてます。

```scala:build.sbt
testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-F", "10")
```

もしくは、testOnlyで実行するときの引数に渡せます。

```sbt
sbt:Hello> testOnly example.HelloSpec -- -F 10
```

どうも、build.sbtで設定しながら、testOnlyの実行時に渡すということはできない様子です。

# まとめ

Thread.sleepするような箇所が環境依存で、ローカルのハイスペックMacBook Proでは通るテストが、Travisで実行するとTimeoutで落ちちゃうってときに、使うと良さげです。

build.sbtにベタって、10って書いちゃうと、ローカルとTravisでって分けられないので、10ってあたりを環境変数とかにして、.travis.ymlとかに環境変数を指定するみたいな運用になると良いのかなって思ってます。

```scala:build.sbt
testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-F", sys.env.getOrElse("SBT_TEST_TIME_FACTOR", "1"))
```

以上です。