はじめに
scalaTestでRSpecのノリでbeforeとかを使いまくってたらエラーが出まくったのでまとめ。
RSpecよりもDSLではないscala言語自体の機能を利用している感が有る。
よってRSpecより覚えたらこっちのほうが使いやすい気がする。
fixure共有はテストコードから冗長なコードを大幅に削減する。
よってプロダクトコードを書くときは必須の知識。
内容はほぼ公式の情報がベース。
Sharing fixtures
選択肢まとめ
手法 | テスト後クリーンアップ | suiteごとの使用fixture変更 | 実行タイミング | fixture組み合わせ | テストコードが触るfixture object の用意 |
---|---|---|---|---|---|
get-fixture methodsパターン | ✕ | ○ | suite中いつでも | ✕ | ○ |
fixture-context objectsパターン | ✕ | ○ | suite中いつでも | ○ | ○ |
withFixture(NoArgTest)のoverride | ○ | ✕ | suite開始直後・終了直前 | △ | ✕ |
loan-fixture methodsパターン | ○ | ○ | suite開始直後・終了直前 | ○ | ○ |
withFixture(OneArgTest)のoverride | ○ | ✕ | suite開始直後・終了直前 | △ | ○ |
BeforeAndAfterのmixin | ○ | ✕ | suite開始直前・終了直後 | ✕ | △ |
withFixtureを使用したstackable trait | ○ | ✕ | suite開始直後・終了直前 | ○ | ○ |
BeforeAndAfterEach/Allを使用したstackable trait | ○ | ✕ | suite開始直前・終了直後 | ○ | ○ |
Scala自体のリファクタリング
get-fixture methodsパターン
- 作成したばかりのインスタンスを取得するパターン
- テスト後のクリーンアップはできない
fixture-context objectsパターン
- Fixtureのメソッドとフィールドをtraitに置くパターン
- テストによって様々な組み合わせのfixtureを使用するときに使う
- テスト後のクリーンアップはできない
loan-fixture methodsパターン
- テストによって様々な組み合わせのfixtureを使用するときに使う
- テスト後のクリーンアップまでできる
withFixtureのoverride
withFixture(NoArgTest)のoverride
- 全て、もしくは殆どのテストが同じfixtureを共有するときに推奨される
- 全て、もしくは殆どのテストの開始直後と終了直前に副作用を起こしたいときに使用する
- テストの出力を変換する
- テストのリトライ
- テストの名前・タグ・テストデータに基づいて戦略を決定する
- 使用するべきでないときと代わりに用いる戦略
- 異なるテストは異なるfixtureを必要とする場合(Scala自体のリファクタリングを行う)
- Fixture中の例外はテストを失敗させる代わりに、中断させるのが望ましい場合(before-and-after traitを使用する)
- テストにオブジェクトを渡したいとき(override withFixture(OneArgTest)を使用する)
withFixture(OneArgTest)のoverride
- 全て、もしくは殆どのテストにfixture objectをパラメタとして渡したいとき
Before-and-after traitのmixin
BeforeAndAfter
- テストの開始時・終了時というより、開始前・終了後に副作用を起こしたいときに仕様する
- ボイラープレートバスター
BeforeAndAfterEach
- 基本はBeforeAndAfterを同じ
- 異なる点はfixtureを組合わせて使用できる
各共有方法の詳細
Get-fixture methods
概要
- mutableなオブジェクトをテスト間で使いまわしたいとき使用
- クリーンアップはできない
- Get-fixture methodsをcallしたときに毎回新しいインスタンスをローカル変数に取得
- 取得したインスタンスに変更を加えながらテスト
例
package org.scalatest.examples.flatspec.getfixture
import org.scalatest.FlatSpec
import collection.mutable.ListBuffer
class ExampleSpec extends FlatSpec {
// get-fixtureメソッド
def fixture =
new {
val builder = new StringBuilder("ScalaTest is ")
val buffer = new ListBuffer[String]
}
"Testing" should "be easy" in {
val f = fixture // ローカル変数として保持
f.builder.append("easy!")
assert(f.builder.toString === "ScalaTest is easy!")
assert(f.buffer.isEmpty)
f.buffer += "sweet"
}
it should "be fun" in {
val f = fixture // ローカル変数として保持
f.builder.append("fun!")
assert(f.builder.toString === "ScalaTest is fun!")
assert(f.buffer.isEmpty)
}
}
もしテストごとに設定値を用いてインスタンス化を行いたいときにはget-fixture methodのパラメタとして宣言してそれを使用する
Fixture-context objects
概要
- テストごとに異なる組合わせのfixtureを使用したいときに使うパターン
- Fixtureだけを保持するtraitを複数用意し、testにmixinする
例
package org.scalatest.examples.flatspec.fixturecontext
import collection.mutable.ListBuffer
import org.scalatest.FlatSpec
class ExampleSpec extends FlatSpec {
// fixtureを保持するtraitを複数定義
trait Builder {
val builder = new StringBuilder("ScalaTest is ")
}
trait Buffer {
val buffer = ListBuffer("ScalaTest", "is")
}
// StringBuilder fixtureを必要とする
"Testing" should "be productive" in new Builder {
builder.append("productive!")
assert(builder.toString === "ScalaTest is productive!")
}
// ListBuffer[String] fixtureを必要とする
"Test code" should "be readable" in new Buffer {
buffer += ("readable!")
assert(buffer === List("ScalaTest", "is", "readable!"))
}
// StringBuilderとListBufferの両方を必要とする
it should "be clear and concise" in new Builder with Buffer {
builder.append("clear!")
buffer += ("concise!")
assert(builder.toString === "ScalaTest is clear!")
assert(buffer === List("ScalaTest", "is", "concise!"))
}
}
Overriding withFixture(NoArgTest)
概要
- get-fixture methodsパターン、fixture-context objectsパターンはクリーンアップができない
- fixture objectをテストに渡す必要はなく、開始時と終了時に副作用を起こしたいときに使用する
-
Suite
trait内に定義されているライフサイクルメソッド
実装詳細
// Suite trait内のデフォルト実装
protected def withFixture(test: NoArgTest) = {
test()
}
// overrideした実装
override def withFixture(test: NoArgTest) = {
// テストの実行前箇所にセットアップを仕込める
try super.withFixture(test) // テストの実行
finally {
// クリーンアップ
}
}
-
Suite
のrunTest
メソッドは引数なしのテスト関数をwithFixture(NoArgTest)
に渡す - テストを実行するのがwithFixtureの責務
- そのため
withFixture
をoverrideしテスト前・後に副作用を仕込む
- そのため
- withFixtureは合成可能なようにデザインできる
- そのためtest関数を直接は呼ばない
- superのwithFixtureで包んで呼ぶ
- super側で定義されてた副作用も発火する
- withFixtureに渡されるNoArgTestはTestDataを保持するため、それらを副作用中で利用できる
例
package org.scalatest.examples.flatspec.noargtest
import java.io.File
import org.scalatest._
class ExampleSpec extends FlatSpec {
override def withFixture(test: NoArgTest) = {
super.withFixture(test) match {
case failed: Failed =>
val currDir = new File(".")
val fileNames = currDir.list()
// superが失敗した場合working directoryのスナップショットをロギング
info("Dir snapshot: " + fileNames.mkString(", "))
failed
case other => other
}
}
"This test" should "succeed" in {
assert(1 + 1 === 2)
}
it should "fail" in {
assert(1 + 1 === 3)
}
}
実行結果
scala> new ExampleSuite execute
ExampleSuite:
This test
- should succeed
- should fail *** FAILED ***
2 did not equal 3 (<console>:33)
+ Dir snapshot: hello.txt, world.txt
loan-fixture methods
概要
- fixutre objectをテストに渡したく、テスト後のクリーンアップをしたいときのパターン
- 異なるテストは異なるfixtureを必要とするとき使用
- 複数のloan-fixture methodsを合成することが可能
- テストごとにfileやdbを切り分けられるため安全
- テストの並行実行が可能になる
例
package org.scalatest.examples.flatspec.loanfixture
import java.util.concurrent.ConcurrentHashMap
object DbServer { // StringBufferでDBをシミュレート
type Db = StringBuffer
private val databases = new ConcurrentHashMap[String, Db]
// db作成
def createDb(name: String): Db = {
val db = new StringBuffer
databases.put(name, db)
db
}
// レコード削除
def removeDb(name: String) {
databases.remove(name)
}
}
import org.scalatest.FlatSpec
import DbServer._
import java.util.UUID.randomUUID
import java.io._
class ExampleSpec extends FlatSpec {
def withDatabase(testCode: Db => Any) {
val dbName = randomUUID.toString
val db = createDb(dbName) // fixture作成
try {
db.append("ScalaTest is ") // fixtureセットアップ
testCode(db) // testにfixtureをloan
}
finally removeDb(dbName) // fixtureのクリーンアップ
}
def withFile(testCode: (File, FileWriter) => Any) {
val file = File.createTempFile("hello", "world") // fixture作成
val writer = new FileWriter(file)
try {
writer.write("ScalaTest is ") // fixtureセットアップ
testCode(file, writer) // testにfixtureをloan
}
finally writer.close() // fixtureをクリーンアップ
}
// file fixtureが必要
"Testing" should "be productive" in withFile { (file, writer) =>
writer.write("productive!")
writer.flush()
assert(file.length === 24)
}
// database fixtureが必要
"Test code" should "be readable" in withDatabase { db =>
db.append("readable!")
assert(db.toString === "ScalaTest is readable!")
}
// databaseとfile両方のfixtureが必要
it should "be clear and concise" in withDatabase { db =>
withFile { (file, writer) => // loan-fixture methodsの合成
db.append("clear!")
writer.write("concise!")
writer.flush()
assert(db.toString === "ScalaTest is clear!")
assert(file.length === 21)
}
}
}
Overriding withFixture(OneArgTest)
概要
- 全て、もしくは殆どのテストが同じfixtureを必要とする場合に使用する
- Loan-fixture methodsよりも簡潔に書ける
- 基本的な実装方法はwithFixture(NoArgTest)のoverrideと同じ
- withFixture(NoArgTest)とも合成可能なようにOneArgTestをNoArgTestに変換してから実行するのがベストプラクティス
- より詳しいテクニック
例
package org.scalatest.examples.flatspec.oneargtest
import org.scalatest.fixture
import java.io._
class ExampleSpec extends fixture.FlatSpec {
case class FixtureParam(file: File, writer: FileWriter)
def withFixture(test: OneArgTest) = {
val file = File.createTempFile("hello", "world") // fixture作成
val writer = new FileWriter(file)
val theFixture = FixtureParam(file, writer)
try {
writer.write("ScalaTest is ") // fixtureのセットアップ
withFixture(test.toNoArgTest(theFixture)) // testにfixtureをloan & OneArgTestをNoArgTestに変換
}
finally writer.close() // fixtureのクリーンアップ
}
"Testing" should "be easy" in { f =>
f.writer.write("easy!")
f.writer.flush()
assert(f.file.length === 18)
}
it should "be fun" in { f =>
f.writer.write("fun!")
f.writer.flush()
assert(f.file.length === 17)
}
}
BeforeAndAfterのmixin
概要
- 今までのfixture共有パターンはどれもtest中に実行を挟む
- セットアップやクリーンアップで例外が発生してもテストの失敗とみなされる
- BeforeAndAfterはテストの前と後で実行を挟む
- セットアップとクリーンアップの例外はテスト自体を中止する
- 複数のfixtureコードを合成することはできない
例
import org.scalatest._
import collection.mutable.ListBuffer
class ExampleSpec extends FlatSpec with BeforeAndAfter {
val builder = new StringBuilder
val buffer = new ListBuffer[String]
// 各テスト前に実行される
before {
builder.append("ScalaTest is ")
}
// 各テスト後に実行される
after {
builder.clear()
buffer.clear()
}
"Testing" should "be easy" in {
builder.append("easy!")
assert(builder.toString === "ScalaTest is easy!")
assert(buffer.isEmpty)
buffer += "sweet"
}
it should "be fun" in {
builder.append("fun!")
assert(builder.toString === "ScalaTest is fun!")
assert(buffer.isEmpty)
}
}
注意
- beforeやafterから直接いじることはできない
- 例のようにミュータブルな状態にアクセスする方法しかない
- この場合並行実行はできない
- 並行実行するにはテストクラスにParallelTestExecutionをミックスインする
- 内部的には各テストケースをインスタンス化したテストクラスとして実行している?
withFixtureを使用したstackable trait
概要
- テストクラスごとに必要とするfixtureが異なる場合に使用する
- withFixtureをoverrideしたstackable traitをmixinして実現
- 実行は各テスト開始時/終了時
例
package org.scalatest.examples.flatspec.composingwithfixture
import org.scalatest._
import collection.mutable.ListBuffer
trait Builder extends SuiteMixin { this: Suite =>
val builder = new StringBuilder
abstract override def withFixture(test: NoArgTest) = {
builder.append("ScalaTest is ")
try super.withFixture(test) // super.withFixtureを呼び出してstackableに
finally builder.clear()
}
}
trait Buffer extends SuiteMixin { this: Suite =>
val buffer = new ListBuffer[String]
abstract override def withFixture(test: NoArgTest) = {
try super.withFixture(test) // super.withFixtureを呼び出してstackableに
finally buffer.clear()
}
}
class ExampleSpec extends FlatSpec with Builder with Buffer {
"Testing" should "be easy" in {
builder.append("easy!")
assert(builder.toString === "ScalaTest is easy!")
assert(buffer.isEmpty)
buffer += "sweet"
}
it should "be fun" in {
builder.append("fun!")
assert(builder.toString === "ScalaTest is fun!")
assert(buffer.isEmpty)
buffer += "clear"
}
}
実行順序はtraitの継承順序で決まる
BeforeAndAfterEachとBeforeAndAfterAfterAllを使用したstackable trait
概要
- withFixtureを使用したstackable traitよりも簡潔
- 実行は各テスト前/後
例
package org.scalatest.examples.flatspec.composingbeforeandaftereach
import org.scalatest._
import collection.mutable.ListBuffer
trait Builder extends BeforeAndAfterEach { this: Suite =>
val builder = new StringBuilder
override def beforeEach() {
builder.append("ScalaTest is ")
super.beforeEach() // super.beforeEachを呼んでstackable traitに
}
override def afterEach() {
try super.afterEach() // super.beforeEachを呼んでstackable traitに
finally builder.clear()
}
}
trait Buffer extends BeforeAndAfterEach { this: Suite =>
val buffer = new ListBuffer[String]
override def afterEach() {
try super.afterEach() // super.beforeEachを呼んでstackable traitに
finally buffer.clear()
}
}
class ExampleSpec extends FlatSpec with Builder with Buffer {
"Testing" should "be easy" in {
builder.append("easy!")
assert(builder.toString === "ScalaTest is easy!")
assert(buffer.isEmpty)
buffer += "sweet"
}
it should "be fun" in {
builder.append("fun!")
assert(builder.toString === "ScalaTest is fun!")
assert(buffer.isEmpty)
buffer += "clear"
}
}