KajMagnus
KajMagnus

Reputation: 11666

Access ScalaTest test name from inside test?

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?
      }

Upvotes: 13

Views: 5627

Answers (5)

Abbas Gadhia
Abbas Gadhia

Reputation: 15100

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
}

Upvotes: 3

ldeck
ldeck

Reputation: 227

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")
  }
}

Upvotes: 0

KajMagnus
KajMagnus

Reputation: 11666

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)
  }
}

Upvotes: 1

Bill Venners
Bill Venners

Reputation: 3669

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")
    }
  }
}

Upvotes: 13

Jakozaur
Jakozaur

Reputation: 1977

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.

Upvotes: 3

Related Questions