Access ScalaTest test name from inside test?
Asked Answered
O

5

13

Is it possible to access the name of the currently executing test, from within a ScalaTest test? (And how would I do it?)

Background:

I'm testing that my Data Access Object eventually throws an OverQuotaException if a user e.g. creates too many pages. These tests take rather long to run. To feel happier, I'd like to print the progress to stdout — and since there are quite many tests, I'd like to include the test name in the output, so I know what test is currently being run.

(I didn't find any seemingly relevant function here: http://www.artima.com/docs-scalatest-2.0.M5/#org.scalatest.FreeSpec )

Example:

  "QuotaCharger can" - {
    "charge and decline quota consumers" - {

      "charge a per site IP number (guest user)" in {
         // ... Here, a guest user post very many comments until it's over quota.
         // This takes a little while, and there are many similar tests.

         // ---> Here <--- I'd like to access the string:
         //   "charge a per site IP number (guest user)",
         //  is that possible somehow?
      }
Oblige answered 12/2, 2013 at 11:15 Comment(0)
E
13

The intended way to do that is to override withFixture and capture the test data. In this use case, it is better to override withFixture in fixture.FreeSpec so you can pass the test data into each test rather than using a var. Info on that is here:

http://www.artima.com/docs-scalatest-2.0.M5/org/scalatest/FreeSpec.html#withFixtureNoArgTest

When I saw your question this morning I realized ScalaTest should have a trait that does this, so I just added one. It will be in 2.0.M6, the next milestone release, but in the meantime you can use a local copy. Here it is:

import org.scalatest._

/**
 * Trait that when mixed into a <code>fixture.Suite</code> passes the
 * <code>TestData</code> passed to <code>withFixture</code> as a fixture into each test.
 *
 * @author Bill Venners
 */
trait TestDataFixture { this: fixture.Suite =>

  /**
   * The type of the fixture, which is <code>TestData</code>.
   */
  type FixtureParam = TestData

  /**
   * Invoke the test function, passing to the the test function to itself, because
   * in addition to being the test function, it is the <code>TestData</code> for the test.
   *
   * <p>
   * To enable stacking of traits that define <code>withFixture(NoArgTest)</code>, this method does not
   * invoke the test function directly. Instead, it delegates responsibility for invoking the test function
   * to <code>withFixture(NoArgTest)</code>.
   * </p>
   *
   * @param test the <code>OneArgTest</code> to invoke, passing in the
   *   <code>TestData</code> fixture
   */
  def withFixture(test: OneArgTest) {
    withFixture(test.toNoArgTest(test))
  }
}

You would use it like this:

import org.scalatest._

class MySpec extends fixture.FreeSpec with TestDataFixture {
  "this technique" - {
    "should work" in { td =>
      assert(td.name == "this technique should work")
     }
    "should be easy" in { td =>
      assert(td.name == "this technique should be easy")
    }
  }
}
Effeminacy answered 12/2, 2013 at 16:8 Comment(8)
I think I prefer the def currentTestName: String approach actually, that you can access from anywhere without passing any parameters along (and modifying function signatures just for debugging purposes). From my point of view, the test name is not really part of the test — it's only debugging info. But when it appears as a parameter to the test (td =>) then it seems to be part of the test.Oblige
What if your test takes a "real" fixture parameter? Then either you'd need to accept 2 parameters to each test? "test name" in { case (td, realTestData) => ... }. Or perhaps it would not be possible to also pass realTestData to the test? Because the one and only parameter passed to the test would be the one that supplied the test name (for debugging purposes).Oblige
Well, keep in mind that currentTestName only works if tests are run sequentially, which will be true unless you mix in ParallelTestExecution. In that case, you could just set a var in the withFixture in regular FreeSpec. If tests are executed sequentially, hat approach would also work, and does sound less intrusive if this is only for debugging temporarily. I was assuming this was going to be there forever.Effeminacy
If you wanted the test name and also some realFixture (let's call it that instead of realTestData), then you'd need to bundle it into one object. But you can just make one fixture class with the pieces you need so your signature is still simple, like: "test name" in { fx => ... fx.testName ... fx.realFixture ... }Effeminacy
Okay, keeping concurrent tests in mind, I suppose this must be the best approach. — You were correct in that I was going to keep the test-name stuff forever (although it's only for debugging). — "realTestData", what a funny name, when I think about it.Oblige
Hmm, def currentTestName: String could in some way use a ThreadLocal, to make it work with concurrent tests? That'd probably be the approach I'd prefer the most I think. (But my test works fine right now so for me it no longer matters :-))Oblige
I tried this with 3.0.0 and I got not found: type NoArgTestImpetrate
Does not compile. Please update this to ScalaTest v 3.xBushel
W
3

Create your own trait, let say RichFreeSpec.

trait RichFreeSpec extends Free {
  protected final class RichFreeSpecStringWrapper(name: scala.Predef.String) {
    def in(f: String => scala.Unit) {
      def f2 = f(name)
      new WordSpecStringWrapper(string).in(f2)
    }
  }  

  protected implicit def convertToRichFreeSpecStringWrapper(n: scala.Predef.String): = {
    new RichFreeSpecStringWrapper(n)
  }
}

Than just use:

"sth" in { testName => 
   ...
 }

Of course, you can go further and implement the full name hierarchy.

Winger answered 12/2, 2013 at 13:59 Comment(4)
Thanks! Actually it didn't work: each time "most recent test name" in { ... } runs, currentTestName is changed to "most recent test name", and when the actual test blocks (inside {...}) runs, the currentTestName is always the same, namely "most recent test name".Oblige
Thanks to your answer, I found the relevant classes and could come up with something :-) Therefore +1, because the answer was useful. (... I'll accept my own answer after 2 days (some limit))Oblige
Sorry for my mistake. Fixed.Winger
Okay, interesting with 2 solutions to the problem. I prefer the other solution with a currentTestName field actually, because then I can access that field from anywhere (without having to pass along the test name).Oblige
E
3

You can use BeforeAndAfterEachTestData for what you need.

If you need access to the test case name in the beforeEach or afterEach method.

class MyTestSuite with AnyFunSuiteLike with BeforeAndAfterEachTestData {

    override def beforeEach(testData: TestData): Unit = {
        testData.name // do whatever.
        super.beforeEach(testData)
    }
}

If you need access to the test case name in the test case itself, then you can use a thread local approach

private val currentTestCaseName = new ThreadLocal[String]

override def beforeEach(testData: TestData): Unit = {
    currentTestCaseName.set(testData.name)
    super.beforeEach(testData)
}

test("fancy test") {
    currentTestCaseName.get() // do whatever
}
Erubescence answered 1/6, 2021 at 10:47 Comment(0)
O
1

Here's a solution. Extend this class instead of FreeSpec. License: CC0.

Edit: This doesn't work with concurrent tests though.

(The difference between this approach and the other answer, is that 1) here there's a currentTestName field, and in the other answer the test name is passed to the test body, and 2) this test name includes all test branch names concatenated + the actual test name, whereas the other answer's test name is exactly the test name (without test branch names).)

(Ooops, you'd need to use getOrElse ... instead of my lovely getOrDie.)

/**
 * Adds a field `currentTestName` that you can use inside a FreeSpec test,
 * if you for example have many tests that take rather long, and you wonder
 * which one is currently running.
 */
trait RichFreeSpec extends FreeSpec {

  private var _currentTestName: Option[String] = None
  def currentTestName = _currentTestName getOrDie "DwE90RXP2"

  protected override def runTest(testName: String, args: org.scalatest.Args) {
    _currentTestName = Some(testName)
    super.runTest(testName, args)
  }
}
Oblige answered 12/2, 2013 at 15:40 Comment(0)
A
0

If the intent is to be able to access the test name from anywhere, as was suggested by @kajmanus in previous comments, a ThreadLocal fits the bill nicely.

You could define a case class to store the info you require for the current test context. e.g.,

case class TestContext(name: Option[String] = None)

object TestContext {
  val currentTest: ThreadLocal[TestContext] =
    ThreadLocal.withInitial(() => TestContext())
}

Then define a trait your various specs will extend. e.g.,

trait BaseFunSpec
  extends AnyFunSpec
  ...
{
  override protected def withFixture(test: NoArgTest): Outcome = {
    try {
      TestContext.currentTest.set(TestContext(name = Some(test.name)))
      super.withFixture(test)
    } finally {
      TestContext.currentTest.remove()
    }
  }
}

Finally, you can access the current test context you've set for the current thread (which in this example is purely the test name) from anywhere within the current thread as needed. e.g.,

def cachedResults(bytes: Array[Byte], fileType: String): Unit = {
  TestContext.currentTest.get().name match {
    case Some(testname) => 
      import scala.util.Using
      val file = new File("target", s"${testname}.${fileType}")
      Using(new BufferedOutputStream(new FileOutputStream(file))) { os =>
        os.write(bytes)
      }
    case None => throw new IllegalStateException("Unknown test context")
  }
}

This will work whether you're running tests in parallel or not, assuming you're not processing things asynchronously (i.e., in another thread).

A cleaner usage of this is to create purposed actors. e.g.,

case class TestContext(name: Option[String] = None)

object TestContext {
  val currentTest: ThreadLocal[TestContext] = ThreadLocal.withInitial(() => TestContext())

  class TestNamer {
    def currentName: String = currentTest.get().name match {
      case Some(testname) => testname
      case None => throw new IllegalStateException("No test context available")
    }
  }

  class TestContextWriter(testNamer: TestNamer = new TestNamer()) {
    def cachedBytes(bytes: Array[Byte], extension: String): Array[Byte] = {
      import java.io.{BufferedOutputStream, File, FileOutputStream}
      import scala.util.Using

      val file = new File("target", s"${testNamer.currentName}.${extension}")

      Using(new BufferedOutputStream(new FileOutputStream(file))) { outstream =>
        outstream.write(bytes)
      }

      bytes
    }
  }
}

And inject as needed:

trait BaseFunSpec {
  val testContextWriter = new TestContextWriter()

  def fetchRawResults(...): Array[Byte] = {
    ...
    testContextWriter.cachedBytes(bytes, "pdf")
  }
}
Alys answered 14/2, 2021 at 17:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.