12

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?
      }
KajMagnus
  • 11,308
  • 15
  • 79
  • 127

5 Answers5

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")
    }
  }
}
Bill Venners
  • 3,549
  • 20
  • 15
  • 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. – KajMagnus Feb 13 '13 at 01:40
  • 1
    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). – KajMagnus Feb 13 '13 at 01:42
  • 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. – Bill Venners Feb 13 '13 at 03:45
  • 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 ... } – Bill Venners Feb 13 '13 at 03:47
  • 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. – KajMagnus Feb 13 '13 at 05:44
  • 2
    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 :-)) – KajMagnus Feb 13 '13 at 06:55
  • 1
    I tried this with 3.0.0 and I got `not found: type NoArgTest` – Jackie Jun 02 '17 at 15:19
  • Does not compile. Please update this to ScalaTest v 3.x – user4955663 Sep 01 '22 at 21:54
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.

Jakozaur
  • 1,957
  • 3
  • 18
  • 20
  • 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"`. – KajMagnus Feb 12 '13 at 15:36
  • 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)) – KajMagnus Feb 12 '13 at 15:38
  • Sorry for my mistake. Fixed. – Jakozaur Feb 12 '13 at 15:42
  • 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). – KajMagnus Feb 12 '13 at 15:50
2

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
}
Abbas Gadhia
  • 14,532
  • 10
  • 61
  • 73
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)
  }
}
KajMagnus
  • 11,308
  • 15
  • 79
  • 127
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")
  }
}
ldeck
  • 227
  • 1
  • 10