LoginSignup
11
5

More than 3 years have passed since last update.

ScalaTestでのfixture共有方法完全集

Last updated at Posted at 2019-10-12

はじめに

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 {
    // クリーンアップ
  }
}
  • SuiterunTestメソッドは引数なしのテスト関数を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"
  }
}
11
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
5