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

scalatestでより構造的なbeforeを書きたい

More than 1 year has passed since last update.

問題

RSpecのデフォルト挙動のようにscalatestのFunSpecで各describeレベルごとにbeforeを行いたい。

class MathTest extends FunSpecEx with Matchers {

  var subject: Int = _

  describeWithBefore("0で初期化すれば"){ subject = 0 } {
    it("0になる") {
      subject should be(0)
    }

    describeWithBefore("5足せば"){ subject += 5 } {
      it("5になる") {
        subject should be(5)
      }

      describeWithBefore("さらに3引けば"){ subject -= 3 } {
        it("2になる") {
          subject should be(2)
        }
        it("その結果は偶数である") {
          (2 % 2) should be (0)
        }
      }

      describeWithBefore("その上で-1掛けると"){ subject *= -1 } {
        it("-5になる") {
          subject should be(-5)
        }
      }
    }
  }

}

しかし、scalatestのbefore(fixture)はdescribeのレベルと対応付けるような機能はない

解決策

上のように書くためのクラスを作ったのでこれを使う。

import org.scalatest._
import org.scalactic._

import scalaz._
import Scalaz._

class FunSpecEx extends FunSpec {

  private[this] type Description = (String, () => Unit)
  private[this] type DescriptionOrTestTitle = \/[Description, String]
  private[this] var tree: Tree[DescriptionOrTestTitle] = ("", () => ()).left[String].node()

  override protected val it: ItWord = new ItWord() {
    override def apply(specText: String, testTags: Tag*)(testFun: => Any /* Assertion */)(implicit pos: source.Position): Unit = {
      tree = tree.loc.insertDownLast(\/-(specText).asInstanceOf[DescriptionOrTestTitle].leaf).toTree
      super.apply(specText, testTags: _*)(testFun)(pos)
    }
  }

  protected[this] def describeWithBefore(description: String)
    (before: => Unit)
    (fun: => Unit)
    (implicit pos: org.scalactic.source.Position): Unit = {
    val backup = tree
    tree = (description, before _).left[String].node()
    super.describe(description)(fun)(pos)
    tree = backup.loc.insertDownLast(tree).toTree
  }

  protected override def runTest(testName: String, args: Args): Status = {
    val targetTestLeaf: TreeLoc[DescriptionOrTestTitle] = tree.loc.find { treeLoc =>
      val strOfDescriptionAndTest = treeLoc.path.reverse.map {
        case -\/(yyy) => yyy._1
        case \/-(xxx) => xxx
      }
      strOfDescriptionAndTest.tail.mkString(" ") == testName
    }.get
    val descriptions: Seq[DescriptionOrTestTitle] = targetTestLeaf.path.reverse
    val befores: Seq[() => Unit] = descriptions.map {
      case -\/(yyy) => yyy._2
      case \/-(_) => () => ()
    }
    for (before <- befores) before()
    super.runTest(testName, args)
  }

}

出力はこんな感じ

[info] Compiling 1 Scala source to /home/kbigwheel/code/_sandbox/2018-09-17-scalatest-structure/target/scala-2.12/test-classes...
[info] FunSpecEx:
[info] CubeCalculatorTest:
[info] 0で初期化すれば
[info] - 0になる
[info]   5足せば
[info]   - 5になる
[info]     さらに3引けば
[info]     - 2になる
[info]     - その結果は偶数である
[info]     その上で-1掛けると
[info]     - -5になる
[info] Run completed in 333 milliseconds.
[info] Total number of tests run: 5
[info] Suites: completed 2, aborted 0
[info] Tests: succeeded 5, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.

蛇足

これが必要 = テスト対象がmutableということなので、scalaの理想的には邪道。
ただしテスト対象にDB状態が絡んでいたり、アプリケーション全体をテストする場合にはimmutableにはほぼできないのでこれが役に立ちます。

ある程度こなれたらライブラリ化もしくはscalatest本体へPR作ろうと思っています。

ライブラリ化しました

https://search.maven.org/artifact/com.github.bigwheel/scalatest-structured-before_2.12/1.0/jar

bigwheel
speee
株式会社Speeeは「解き尽くす。未来を引きよせる。」というミッションを実現すべく、中長期的な目線で企業価値を最大化させていくため、組織・事業のStyleを大切にした永続的な価値創造を目指しています。
https://www.speee.jp/
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