Help us understand the problem. What is going on with this article?

ScalaTest入門

はじめに

Webcrewアドベントカレンダー5日目の記事です。

2019年度新卒の@verdoyantが5日目を担当します。
テストコードを学んでいこうと思い、ScalaTestについて調べたので簡単にまとめました。

対象

テストを書き始めてみようかなと思っている人向け

ScalaTest 概要

  • Scalaでは一番使用されている
  • 使いやすく拡張しやすい

環境

sbt 1.3.4
scala 2.13.1
PlayFramework 2.7.3

導入方法(PlayFramework 2.7.3 + sbt)

build.sbt に以下を追記

build.sbt
libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "4.0.3" % "test" //PlayFrameworkを使う場合
libraryDependencies ++= Seq(//PlayFrameworkを使わない場合
   "org.scalactic" %% "scalactic" % "3.0.8",
   "org.scalatest" %% "scalatest" % "3.0.8" % "test"
)

テスト例

test ディレクトリ以下に下記ファイルを作成

SetSuite.scala
import org.scalatest._
import org.scalatestplus.play._  

class SetSuite extends FunSuite {

  test("An empty Set should have size 0") {
    assert(Set.empty.size == 0)
  }

  test("Invoking head on an empty Set should produce NoSuchElementException") {
    assertThrows[NoSuchElementException] {
      Set.empty.head
    }
  }
}

プロジェクトのルートディレクトリでsbt test後
app(src/main)ディレクトリ下とtestディレクトリ下をコンパイル後にテストが行われます。
するとこんな感じの表示が出ると思います。出なかったら多分logの設定がおかしいと思われます。

[info] SetSuite:
[info] - An empty Set should have size 0
[info] - Invoking head on an empty Set should produce NoSuchElementException
[info] ScalaTest
[info] Run completed in 248 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.
[info] Passed: Total 2, Failed 0, Errors 0, Passed 2

結果を見るとテストが2つ実行されて、2つ通ったという状態です。
assertが失敗するとfailedにカウントされ、sbt.TestsFailedException: Tests unsuccessfulというerrorを返します。
assertThrowsを使うテストはSuites: completedにカウントされないということも注意が必要です。
cancelledやignoredなどは特定の書き方をしないと出てこないので、基本的にはPassedとFailedを見ていればいいです。

テストコードの書き方

記法

FunSuiteをextendsするとtest(""){ assert()}という記法になるのですが、FlatSpecやWordSpecをextendsすると別の書き方ができます。
PlaySpec, WordSpecやFlatSpecであれば、一つのメソッドを別の条件でテストするというのが分かりやすく書けるので基本的にこれらを使うのが個人的には良いと思います。
ちなみにWordSpecで使える機能はPlaySpecでも使えます。(PlaySpecはWordSpecをextendsしたもの)
参考 Selecting testing styles for your project

テストケースの書き方

テストケースはassert(Hoge.hode() == 1) のようにassert文の中に条件文を書いていく形になります。
PlaySpecを使っている場合Hoge.hode() mustBe 1のようにも書けます。

SetSuite.scala
class SetSuite extends PlaySpec {

  "An empty Set should have size 0" in {
    Set.empty.size mustBe 0
  }
  "Invoking head on an empty Set should produce NoSuchElementException"  in {
    a [NoSuchElementException] must be thrownBy {
      Set.empty.head
    }
  }
}

mustassertはどちらも同じ値であるか見る以外にも型が等しいかやコンパイル可能かどうかなども見ることができます。

"TypeTest" in{
    1 mustBe a [Int]
  }

assertを使う書き方はリンク参照 Using assertions
mustを使う書き方はリンク参照 MustMatchers

テスト実行方法/オプション

テストタスク

  • testOnly

テストはsbt testで実行できますが、あるテストだけ実行したいときはsbt testOnly SetSuiteのように特定のテストクラスを指定することができます。
testOnlyタスクにはワイルドカードも使えるのでSetSuite01, SetSuite02, SeqSuite01がある場合に
sbt testOnly SetSuite*でSetSuite01, SetSuite02だけを実行できます。

  • testQuick

sbt testQuickで前回失敗したり、実行されていなかったテストのみを実行できます。

オプション引数

testOnlyは引数付きで実行でき、build.sbtに以下のように記述することで全てのテストが引数付きで実行されます。

testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-f", output.txt")
  • - z

testOnly TestClass -- -z "TestCase"でTestClass内の"TestCase"を含むテストのみ行うことができます。

  • -f filename

テスト結果をfilenameに書き出してくれます。

そのほかにも多くのオプションがあり、以下を参照
Using the Runner

mock

実際に作成したクラスなどテストをするうえで、機能としては使わなくともオブジェクトを用意しなければならないことが多々あります。
その際にmockというものを使うとわざわざインスタンスを作成しなくても、偽装したインスタンスを作成して引数で渡すことでテストの依存する部分が減らせるという利点があります。
mockを作成できるライブラリはいくつかあるのですが、今回はspecs2 + mockitoを使用してみます。

build.sbtに以下を追記

build.sbt
libraryDependencies ++= specs2 % Test //PlayFrameworkの場合
libraryDependencies ++= Seq( //PlayFrameworkなしの場合
  "org.specs2" %% "specs2-core" % "4.8.1" % "test",
  "org.specs2" %% "specs2-mock" % "4.8.1" % "test"
) 

app(main/scala)以下にテスト対象のクラスを含んだファイルを作成します。

Controller.scala
package main

case class Data(date:java.util.Date)

class Controller(data:Data) {
   def getMessage(message:String):String = message+"Controlled"
}

このControllerをテストするためにはDataを引数で渡す必要があります。
しかしgetMessageメソッドには使われていません。
そこでmock[Data]でDataを偽装してControllerに渡してしまいましょう。
testディレクトリ以下に次のテストファイルを作成します。

ControllerSpec.scala
import org.scalatest._
import org.scalatestplus.play._
import org.specs2.mock.Mockito
import main._

class ControllerSpec extends PlaySpec with Mockito {
  "Controllerのテスト" in {
    val expected = "Test"
    val controller = new Controller(mock[Data]) {
      override def getMessage(message:String): String = message + "Controlled"
    }
    controller.getMessage(expected) must include (expected)
  }
}

これを実行するとテストが通ったことを確認できると思います。
mockを使わない場合、このテストケースでは関係のないDataオブジェクトを作成しなくてはなりませんが
mock[Data]を作ることでテストケースに関係ないものを無視できました。

この例だと分かりずらいのですが、他のモジュール、例えばデータベースやAPIに依存するものをテストする際には非常に有効で
APIやデータベースがちゃんと動いているかという懸念点を取り除くことができ、mockによって純粋な機能テストになるという点でmockは活用されています。

Coverage

テストについて学んでいるとコードカバレッジについて目にしたこともあると思います。
ScalaTestを使ってコードカバレッジのレポートを出力するにはproject/plugins.sbtに以下を追記します。

plugin.sbt
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1")

そしてsbt clean coverage testを実行して、次にsbt coverageReportを実行します。
そうするとtarget/scala2.13/scoverage-report以下にhtmlファイルが作成されており、index.htmlを開くとCoverage %などを見ることができます。
では先ほど作ったControllerのテストはどうだったか見てみましょう。キャプチャ.PNG

出来てないですね。先ほど書いたControllerSpecを直しましょう。

ControllerSpec.scala
class ControllerSpec extends PlaySpec with Mockito {
  "Controllerのテスト" in {
    val expected = "Test"
    val controller = new Controller(mock[Data])
    controller.getMessage(expected) must include (expected)
  }
}

テストに使うメソッド自体をoverrideしてしまうとcoverage%が下がり、今回の場合だと意味がないので変更しました。
そしてsbt clean coverage testを実行して、次にsbt coverageReportを実行してindex.htmlを開くと
キャプチャ.PNG
Coverage 100%になりました。良かったですね。
他にもこのレポートは様々な情報があるので実際にScalaTestを動かしてつつ学んでみてください。

まとめ

ScalaTestを使ってテストをする方法をまとめました。
文中にもあるようにPlaySpecはWordSpecをextendsして機能追加したものなのですが、同じように自分用の独自Specを作成することも出来て拡張性があったりします。

上に書いたこと以外にもDIコンテナのGuiceを使うと、よりテストのしやすい依存性の少ないコードが書けるので、もし興味を持ったならばGuiceやDIについて学んでみるのを是非お勧めします。

Webcrewアドベントカレンダー6日目担当は@k5jpWCさんです!
心理学関係について書いてくれるそうです。

参考文献

  • ScalaTest
    ScalaTestの公式、全部英語ですが…
  • sbt
    sbtのテストのdocument,テストの設定など詳しい。
  • Scala研修テキスト
    dwangoの新卒エンジニア向けテキスト。とても分かりやすい。mockの例はこっちのほうがずっとわかりやすい。
  • PlayFrameworkのdocument
    PlayFrameworkのScalaTestのdocument。PlayFrameworkのコントローラやモデルなどのテストでの扱い方など詳しい。

注意点

Windows環境で日本語を表示するとterminalの文字コード制約により文字化けが起きてしまいます。
javaの起動オプションに-Dfile.encoding=MS932を渡すと解消するかもしれないです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした