読者です 読者をやめる 読者になる 読者になる

新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

学習記録:ドワンゴ 新人向けScala研修テキスト

18. テスト

ユニットテスト

ここでは,ユニットテストを小さな単位で自動実行できるテストと定義して解説を行う。
ユニットテストを行う理由は大きく3つあげられる。

  1. 実装前に満たすべき仕様を定義し,要件漏れをなくす
  2. 満たすべき仕様がテストされた状態なら,安心してリファクタリングできる
  3. 全ての機能を実装する前に単体でテストできる

スティングフレームワーク

Scalaで広く利用されているテスティングフレームワークは以下の2つ。

  • Specs2
  • ScalaTest

今回は振舞駆動開発(BDD)をサポートしているフレームワークであるScalaTestを使う。
BDDでは,テスト内にそのプログラムに与えられた機能的な外部仕様を記述させることで,テストが本質的に何をテストしようとしているのかをわかりやすくする手法になる。

テストができるsbtプロジェクトの作成

適当な作業ディレクトリにて,src/main/scalaとsrc/test/scalaの2つのディレクトリを作り,以下のbuild.sbtを用意する。

name := "scalatest_study"

version := "1.0"

scalaVersion := "2.11.8"

libraryDependencies += "org.scalatest" %% "scalatest" % "2.2.6" % "test"

その後,sbt compileを実行して準備完了になる。

Calcクラスとテストを作る

下記の仕様を満たすCalcクラスを作成し,それらをテストしていく。

  • 整数の配列を取得し,それらを足し合わせた整数を返すsum関数を持つ
  • 整数を2つ受け取り,分子を分母で割った浮動小数点を返すdiv関数を持つ
  • 整数を1つ受け取り,素数であるかを返すisPrime関数を持つ

この実装は,以下のようになる。

class Calc {
  def sum(seq: Seq[Int]): Int = seq.foldLeft(0)(_ + _)
  def div(numerator: Int, denominator: Int): Double = {
    if (denominator == 0) throw new ArithmeticException("divide by zero")
    numerator.toDouble / denominator.toDouble
  }
  def isPrime(n: Int): Boolean = {
    if (n < 2) false else !((2 to Math.sqrt(n).toInt) exists (n % _ == 0))
  }
}

続いてsum関数のテストを書いてみる。テストクラスにDiagrammedAssertionsをミックスインし,assertメソッドの引数に期待する条件を記述していく。

import org.scalatest._

class CalcSpec extends FlatSpec with DiagrammedAssertions {
  val calc = new Calc

  "sum関数" should "整数の配列を取得し,それらを足し合わせた整数を返すことができる" in {
    assert(calc.sum(Seq(1, 2, 3)) === 6)
    assert(calc.sum(Seq(0)) === 0)
    assert(calc.sum(Seq(-1, 1)) === 0)
    assert(calc.sum(Seq()) === 0)
  }

  it should "Intの最大を上回った際はオーバフローする" in {
    assert(calc.sum(Seq(Integer.MAX_VALUE, 1)) === Integer.MIN_VALUE)
  }
}

sbt testでテストが実行される。わざと失敗させると以下のような表示になる。

[info] CalcSpec:
[info] sum関数
[info] - should 整数の配列を取得し,それらを足し合わせた整数を返すことができる *** FAILED ***
[info]   assert(calc.sum(Seq(-1, 1)) === 6)
[info]          |    |  ||    |  |   |   |
[info]          |    0  ||    -1 1   |   6
[info]          |       |List(-1, 1) false
[info]          |       0
[info]          Calc@d79df17 (CalcSpec.scala:9)
[info] - should Intの最大を上回った際はオーバフローする
[info] Run completed in 260 milliseconds.
[info] Total number of tests run: 2
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 1, canceled 0, ignored 0, pending 0
[info] *** 1 TEST FAILED ***
[error] Failed tests:
[error] 	CalcSpec
[error] (test:test) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 5 s, completed 2016/08/06 18:07:16

成功時には以下のようになる。

[info] CalcSpec:
[info] sum関数
[info] - should 整数の配列を取得し,それらを足し合わせた整数を返すことができる
[info] - should Intの最大を上回った際はオーバフローする
[info] Run completed in 211 milliseconds.
[info] Total number of tests run: 2
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 4 s, completed 2016/08/06 18:12:04

次に,例外が発生するテストを記述してみる。

  "div関数" should "0で割ろうとした際には実行時例外が投げられる" in {
    intercept[ArithmeticException] {
      calc.div(1, 0)
    }
  }

上記ではゼロ除算の際に投げられる例外をテストしている。
intercept[Exception]という構文で作ったスコープ内で投げられる例外がある場合に成功となり,例外がない場合にはテストが失敗する。

最後にパフォーマンスを保証するテストを書く。

import org.scalatest.concurrent.Timeouts
import org.scalatest.time.SpanSugar._

...

class CalcSpec extends FlatSpec with DiagrammedAssertions wi\
th Timeouts {
  ...

  "isPrime関数" should "100万以下の値の素数判定を1秒以内で処\
理できる" in {
    failAfter(1000 millis) {
      assert(calc.isPrime(9999991))
    }
  }
}

Timeoutというトレイトを利用することで,failAfterという処理時間をテストする機能を利用できるようになる。

モック

モックとは,テストする際に必要となるオブジェクトを偽装して用意出来る機能で,以下のようなモックライブラリが存在する。

  • ScalaMock
  • EasyMock
  • JMock
  • Mockito

ここでは,Mockitoを利用してみる。build.sbtに以下を追記することで利用可能になる。

libraryDependencies += "org.mockito" % "mockito-core" % "1.10.19" % "test"

先ほど作成したCalcクラスのモックを用意して,モックにsumの振る舞いを仕込んで見る。

import org.scalatest._
import org.scalatest.concurrent.Timeouts
import org.scalatest.time.SpanSugar._
import org.scalatest.mock.MockitoSugar
import org.mockito.Mockito._

class CalcSpec extends FlatSpec with DiagrammedAssertions wi\
th Timeouts with MockitoSugar {
  ...

  "Calcのモックオブジェクト" should "振舞を偽装できる" in {
    val mockCalc = mock[Calc]
    when(mockCalc.sum(Seq(3, 4, 5))).thenReturn(12)
    assert(mockCalc.sum(Seq(3, 4, 5)) === 12)
  }
}

MockitoSugarトレイトをミックスインすることで,ScalaTest独自の省略記法を用いてMockitoを利用できるようになる。
val = mockCalc = mock[Calc]でモックオブジェクトを作成し,when(...)で振る舞いを作成している。

上記のようなモックの機能は,実際には時間がかかってしまう通信などの部分を高速に動かすために利用されている。
モックを含め,テスト対象が依存しているオブジェクトを置き換える代用品を,総称してテストダブルと呼ぶ。

コードカバレッジの測定

テストが機能のどれくらいを網羅できているのかを知る方法としてコードカバレッジを計測する方法がある。ここでは,scoverageを利用する。

project/plugins.sbtというファイルを作り,以下のコードを記述する。

resolvers += Classpaths.sbtPluginReleases

addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.3.3")

その後,sbt clean coverage testを実行することで,target/scala-2.11/scoverage-report/index.htmlにレポートが出力される。

コードスタイルチェック

ここまで紹介したテストは,実際にはJenkinsなどのCIツールで実施され,リグレッションを検出するために使われる。
その際に一緒に行われることが多いのがコードスタイルチェックである。

ここでは,ScalaStyleを利用する。
project/plugins.sbtに以下のコードを記述する。

addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.6.0")

その後,sbt scalastyleGenerateConfigを一度だけ実行後,sbt scalastyleを実行することで,ルールに即したエラーや警告が表示される。
ルールの変更はscalastyle-config.xmlで行うことができ,デフォルトではApacheライセンスの記述を入れないと警告が出る設定になっている。