【问题标题】:ScalaTest: Issues with Singleton Object re-initializationScalaTest:单例对象重新初始化的问题
【发布时间】:2011-03-25 12:25:33
【问题描述】:

我正在使用 ScalaTest 测试我用 Scala 编写的解析器。解析器一次处理一个文件,它有一个单例对象,如下所示:

class Parser{...}
object Resolver {...}

我写的测试用例有点像这样

   describe("Syntax:") {
    val dir = new File("tests\\syntax");
    val files = dir.listFiles.filter(
                    f => """.*\.chalice$""".r.findFirstIn(f.getName).isDefined);

    for(inputFile <- files) {
      val parser = new Parser();
      val c = Resolver.getClass.getConstructor();
      c.setAccessible(true);
      c.newInstance();

      val iserror = errortest(inputFile)
      val result = invokeparser(parser,inputFile.getAbsolutePath) //local method
      it(inputFile.getName + (if (iserror)" ERR" else " NOERR") ){
      if (!iserror) result should be (ResolverSuccess()) 
        else if(result.isInstanceOf[ResolverError]) assert(true)
      }
    }
  }

现在在每次迭代中,单例对象 Resolver 中先前迭代的副作用不会被清除。

有没有办法指定scalatest模块重新初始化单例对象?

更新:根据 Daniel 的建议,我更新了代码,还添加了更多细节。

更新:显然是解析器在做一些可疑的事情。在随后的调用中,它不会丢弃先前的 AST。奇怪的。由于这是题外话,我会挖掘更多,可能会使用单独的线程进行讨论,谢谢大家的回答

最终更新:问题出在 Resolver 以外的单例对象上,它位于其他文件中,所以我不知何故错过了它。我能够使用 Daniel Spiewak 的回复解决这个问题。这是一种肮脏的做事方式,但考虑到我的情况以及我正在编写测试代码的事实,这也是唯一的事情,它不会投入生产使用。

【问题讨论】:

  • R里面有没有清理方法,还是希望重新创建?
  • R 中没有清理方法,我无法更改代码。你能在 Scala 中重新创建单例对象吗?
  • 不。他们是严格的单身人士。按需初始化(由 JVM 的类加载决定),然后永久初始化。当然,它们可以具有可变的内部状态,就像任何 Scala 类一样。
  • 假设 Parser 被硬编码为引用常规 Resolver 单例实例,您的更改将不起作用。您正在使用 Daniel 建议的偷偷摸摸的技巧在此处创建它的新实例(然后建议您永远不要使用)。如果 Parser 真的是这样设计的,它使用对 Resolver 的硬编码引用来维护状态(不是一个非常可测试的设计),那么您需要做的是为每个测试加载 Resolver 和 Parser,并使用自定义类加载器。我会在回复中发布。

标签: scala scalatest


【解决方案1】:

根据语言规范,不,没有办法重新创建单例对象。但是,可以反射性地调用单例的构造函数,它会覆盖包含实际单例值的内部 MODULE$ 字段:

object Test

Test.hashCode    // => e.g. 779942019

val c = Test.getClass.getConstructor()
c.setAccessible(true)
c.newInstance()

Test.hashCode    // => e.g. 1806030550

既然我已经和你分享了这个邪恶的秘密,让我警告你永远不要,永远这样做。我会非常非常努力地调整代码,而不是像这样玩鬼鬼祟祟的把戏。但是,如果事情如你所说,而你真的别无选择,这至少是一些事情。

【讨论】:

  • 以这种方式重新初始化单例对象有效,但我的问题仍然存在。我不知道怎么做。基本上,如果我单独测试每个输入文件,它可以正常工作,但如果我一起测试它们,我会出错。如果我更改顺序,我也会收到错误(带有与先前测试用例相关的错误消息!)。我怀疑这可能是因为单例对象具有先前迭代的副作用。
  • 试过了,知道了... java.lang.NoSuchMethodException: com.whatever.SingletonObject$.()
  • 我不得不将上面的@daniel-spiewak 示例更改为val c = Test.getClass.getDeclaredConstructor()
【解决方案2】:

ScalaTest 有几种方法可以让您在测试之间重新初始化事物。但是,如果不了解更多信息,就很难回答这个特定的问题。主要问题是,重新初始化单例对象需要什么?如果在不实例化新的单例对象的情况下无法重新初始化单例对象,那么您需要确保每个测试都重新加载了单例对象,这需要使用自定义类加载器。不过,我很难相信有人会这样设计东西。你能用更多这样的细节更新你的问题吗?稍后我会再看一遍,看看额外的细节是否让答案更明显。

ScalaTest 有一个运行路径,可以为每次运行重新加载类,但不是测试路径。所以你必须自己动手。这里真正的问题是有人以一种不容易测试的方式设计了它。我会在每个测试中使用 URLClassLoader 加载解析器和解析器。这样你每次测试都会得到一个新的解析器。

您需要将 Parser & Resolver 从类路径和运行路径中移除。将它们放入自己的目录中。然后为指向该目录的每个测试创建一个 URLClassLoader。然后在该类加载器上调用 findClass("Parser") 来获取它。我假设 Parser 指的是 Resolver,在这种情况下,JVM 将返回加载 Parser 的类加载器以获取 Resolver,即您的 URLClassLoader。在 Parser 上执行 newInstance 以获取实例。这应该可以解决您的问题,因为您将为每个测试获得一个新的 Resolver 单例对象。

【讨论】:

  • 创建一个新的解析器不会重新初始化单例对象,解析器对象的创建是在循环内。 (检查更新的代码)该代码在一个执行跟踪中接受单个输入文件。
【解决方案3】:

没有答案,但我确实有一个简单的示例,说明您可能希望在哪里重置单例对象,以便在多种潜在情况下测试单例构造。考虑一些愚蠢的事情,比如下面的代码。您可能希望编写测试来验证在未正确设置环境时是否引发异常,并编写测试以验证在未正确设置环境时不会发生异常。我知道,我知道每个人都说,“当环境设置不正确时提供默认值。”但我不想这样做;这会导致问题,因为不会通知您使用了错误的系统。

object RequiredProperties extends Enumeration {
  type RequiredProperties = String

  private def getRequiredEnvProp(propName: String) = {
    sys.env.get(propName) match {
      case None => throw new RuntimeException(s"$propName is required but not found in the environment.")
      case Some(x) => x
    }
  }

  val ENVIRONMENT: String = getRequiredEnvProp("ENVIRONMENT")
}

用法:

Init(RequiredProperties.ENVIRONMENT)

如果我提供了默认值,那么用户永远不会知道它没有被设置并默认为开发环境。或者类似的东西。

【讨论】:

    猜你喜欢
    • 2016-01-06
    • 1970-01-01
    • 1970-01-01
    • 2021-05-23
    • 1970-01-01
    • 2020-10-13
    • 1970-01-01
    • 2023-04-04
    • 2021-04-07
    相关资源
    最近更新 更多