【问题标题】:Access ScalaTest test name from inside test?从测试内部访问 ScalaTest 测试名称?
【发布时间】:2013-01-27 16:22:42
【问题描述】:

是否可以从 ScalaTest 测试中访问当前正在执行的测试的名称? (我该怎么做?)

背景:

我正在测试我的数据访问对象是否最终会抛出OverQuotaException,如果用户例如创建太多页面。这些测试需要很长时间才能运行。为了更开心,我想将进度打印到标准输出——由于有很多测试,我想在输出中包含测试名称,这样我就知道当前正在运行什么测试。

(我在这里没有找到任何看似相关的功能:http://www.artima.com/docs-scalatest-2.0.M5/#org.scalatest.FreeSpec

示例:

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

【问题讨论】:

    标签: scala integration-testing scalatest


    【解决方案1】:

    这样做的预期方法是覆盖 withFixture 并捕获测试数据。在这个用例中,最好在 fixture.FreeSpec 中覆盖 withFixture,这样您就可以将测试数据传递到每个测试中,而不是使用 var。相关信息在这里:

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

    当我今天早上看到你的问题时,我意识到 ScalaTest 应该具有这样做的特性,所以我只是添加了一个。它将在下一个里程碑版本 2.0.M6 中,但同时您可以使用本地副本。这里是:

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

    你会这样使用它:

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

    【讨论】:

    • 我认为我实际上更喜欢def currentTestName: String 方法,您可以从任何地方访问而无需传递任何参数(并且仅出于调试目的而修改函数签名)。从我的角度来看,测试名称并不是测试的一部分——它只是调试信息。但是当它作为测试的参数出现时(td =&gt;),它似乎是测试的一部分。
    • 如果您的测试采用“真实”夹具参数怎么办?那么您是否需要为每个测试接受 2 个参数? "test name" in { case (td, realTestData) =&gt; ... }。或者也许不可能通过realTestData 进行测试?因为传递给测试的唯一参数是提供测试名称的参数(用于调试目的)。
    • 好吧,请记住 currentTestName 仅在测试按顺序运行时才有效,除非您混合使用 ParallelTestExecution,否则这是正确的。在这种情况下,您可以在常规 FreeSpec 中的 withFixture 中设置一个 var。如果测试是按顺序执行的,帽子方法也可以工作,如果这只是为了临时调试,听起来确实不那么具有侵入性。我以为这会永远存在。
    • 如果您想要测试名称和一些 realFixture(我们称之为而不是 realTestData),那么您需要将它捆绑到一个对象中。但是你可以只用你需要的部分制作一个夹具类,这样你的签名仍然很简单,比如: { fx => ... fx.testName ... fx.realFixture ... }中的“测试名称” >
    • 嗯,def currentTestName: String 可以以某种方式使用 ThreadLocal,使其与并发测试一起工作?这可能是我最喜欢的方法。 (但我的测试现在运行良好,所以对我来说不再重要:-))
    【解决方案2】:

    创建你自己的特质,比如说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)
      }
    }
    

    不仅仅是使用:

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

    当然,您可以更进一步,实现全名层次结构。

    【讨论】:

    • 谢谢!实际上它没有用:每次"most recent test name" in { ... }运行时,currentTestName被更改为“最近的测试名称”,而当实际的测试块({...}内部)运行时,currentTestName总是一样的,即"most recent test name"
    • 感谢您的回答,我找到了相关的课程并且可以想出一些东西:-)因此+1,因为答案很有用。 (......我会在 2 天后接受我自己的答案(有限制))
    • 对不起我的错误。固定。
    • 好的,这个问题的 2 个解决方案很有趣。实际上,我更喜欢带有 currentTestName 字段的其他解决方案,因为这样我就可以从任何地方访问该字段(无需传递测试名称)。
    【解决方案3】:

    这里有一个解决方案。扩展这个类而不是 FreeSpec。许可证:CC0

    编辑:但这不适用于并发测试。

    (此方法与其他答案的区别在于 1)这里有一个 currentTestName 字段,在另一个答案中,测试名称被传递给测试主体,并且 2)此测试名称包括所有测试分支名称连接 + 实际测试名称,而另一个答案的测试名称正是测试名称(没有测试分支名称)。)

    (糟糕,您需要使用getOrElse ... 而不是我可爱的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)
      }
    }
    

    【讨论】:

      【解决方案4】:

      如果意图是能够从任何地方访问测试名称,正如 @kajmanus 在之前的 cmets 中所建议的那样,ThreadLocal 非常适合。

      您可以定义一个案例类来存储当前测试上下文所需的信息。例如,

      case class TestContext(name: Option[String] = None)
      
      object TestContext {
        val currentTest: ThreadLocal[TestContext] =
          ThreadLocal.withInitial(() => TestContext())
      }
      

      然后定义一个你的各种规格将扩展的特征。例如,

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

      最后,您可以根据需要从当前线程中的任何位置访问您为当前线程设置的当前测试上下文(在此示例中纯粹是测试名称)。例如,

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

      无论您是否并行运行测试,这都将起作用,假设您没有异步处理事物(即在另一个线程中)。

      更简洁的用法是创建有目的的演员。例如,

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

      并根据需要注入:

      trait BaseFunSpec {
        val testContextWriter = new TestContextWriter()
      
        def fetchRawResults(...): Array[Byte] = {
          ...
          testContextWriter.cachedBytes(bytes, "pdf")
        }
      }
      

      【讨论】:

        【解决方案5】:

        您可以根据需要使用BeforeAndAfterEachTestData

        如果您需要在 beforeEach 或 afterEach 方法中访问测试用例名称。

        class MyTestSuite with AnyFunSuiteLike with BeforeAndAfterEachTestData {
        
            override def beforeEach(testData: TestData): Unit = {
                testData.name // do whatever.
                super.beforeEach(testData)
            }
        }
        

        如果您需要在测试用例本身中访问测试用例名称,那么您可以使用线程本地方法

        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
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-12-10
          • 1970-01-01
          • 1970-01-01
          • 2011-11-05
          • 2012-05-05
          • 1970-01-01
          • 2023-03-13
          • 2014-06-11
          相关资源
          最近更新 更多