【问题标题】:SBT integration test setupSBT 集成测试设置
【发布时间】:2014-03-29 06:47:25
【问题描述】:

我想在我的 SBT + Spray 应用中添加一个集成测试阶段。

理想情况下,它就像 Maven,具有以下阶段:

  • compile: 应用构建完成
  • test: 单元测试已运行
  • pre-integration-test:应用程序在单独的进程中启动
  • integration-test:运行集成测试;他们向后台运行的应用发出请求并验证返回的结果是否正确
  • post-integration-test:之前启动的应用实例已关闭

我在让它工作时遇到了很多麻烦。有没有我可以效仿的可行示例?

1) 单独的“it”代码库:

我首先将"Integration Test" section of the SBT docs 中显示的代码添加到project/Build.scala 的新文件中。

这允许我在“src/it/scala”下添加一些集成测试并使用“sbt it:test”运行它们,但我看不到如何添加pre-integration-test 挂钩。

问题“Ensure 're-start' task automatically runs before it:test”似乎解决了如何设置这样的钩子,但答案对我不起作用(见my comment on there)。

此外,将上述代码添加到我的 build.scala 中,“sbt re-start”任务完全停止工作:它尝试在“it”模式下运行应用程序,而不是在“默认”模式下。

2) “测试”代码库中的集成测试:

我正在使用 IntelliJ,而单独的“it”代码库确实使它感到困惑。它无法编译该目录中的任何代码,因为它认为所有依赖项都丢失了。

我尝试从 SBT 文档中粘贴“Additional test configurations with shared sources”中的代码,但出现编译错误:

[error] E:\Work\myproject\project\Build.scala:14: not found: value testOptions
[error]         testOptions in Test := Seq(Tests.Filter(unitFilter)),

有没有我可以效仿的可行示例?

我正在考虑放弃通过 SBT 进行设置,而是添加一个测试标志来将测试标记为“集成”并编写一个外部脚本来处理这个问题。

【问题讨论】:

    标签: scala sbt integration-testing


    【解决方案1】:

    我现在已经编写了自己的代码来执行此操作。 我遇到的问题:

    • 我发现将我的 build.sbt 转换为 project/Build.scala 文件修复了大部分编译错误(并且通常使编译错误更容易修复,因为 IntelliJ 可以更轻松地提供帮助)。

    • 我能找到的在后台进程中启动应用程序的最佳方法是使用 sbt-start-script 并在新进程中调用该脚本。

    • 在 Windows 上杀死后台进程非常困难。

    我的应用程序中的相关代码发布在下面,因为我认为有些人遇到了这个问题。 如果有人写了一个 sbt 插件来“正确”地做到这一点,我很想听听。

    来自project/Build.scala的相关代码:

    object MyApp extends Build {
      import Dependencies._
    
      lazy val project = Project("MyApp", file("."))
    
        // Functional test setup.
        // See http://www.scala-sbt.org/release/docs/Detailed-Topics/Testing#additional-test-configurations-with-shared-sources
        .configs(FunctionalTest)
        .settings(inConfig(FunctionalTest)(Defaults.testTasks) : _*)
        .settings(
          testOptions in Test := Seq(Tests.Filter(unitTestFilter)),
          testOptions in FunctionalTest := Seq(
            Tests.Filter(functionalTestFilter),
            Tests.Setup(FunctionalTestHelper.launchApp _),
            Tests.Cleanup(FunctionalTestHelper.shutdownApp _)),
    
          // We ask SBT to run 'startScriptForJar' before the functional tests,
          // since the app is run in the background using that script
          test in FunctionalTest <<= (test in FunctionalTest).dependsOn(startScriptForJar in Compile)
        )
        // (other irrelvant ".settings" calls omitted here...)
    
    
      lazy val FunctionalTest = config("functional") extend(Test)
    
      def functionalTestFilter(name: String): Boolean = name endsWith "FuncSpec"
      def unitTestFilter(name: String): Boolean = !functionalTestFilter(name)
    }
    

    这个帮助代码在project/FunctionTestHelper.scala:

    import java.net.URL
    import scala.concurrent.{TimeoutException, Future}
    import scala.concurrent.ExecutionContext.Implicits.global
    import scala.concurrent.duration._
    import scala.sys.process._
    
    /**
     * Utility methods to help with the FunctionalTest phase of the build
     */
    object FunctionalTestHelper {
    
      /**
       * The local port on which the test app should be hosted.
       */
      val port = "8070"
      val appUrl = new URL("http://localhost:" + port)
    
      var processAndExitVal: (Process, Future[Int]) = null
    
      /**
       * Unfortunately a few things here behave differently on Windows
       */
      val isWindows = System.getProperty("os.name").startsWith("Windows")
    
      /**
       * Starts the app in a background process and waits for it to boot up
       */
      def launchApp(): Unit = {
    
        if (canConnectTo(appUrl)) {
          throw new IllegalStateException(
            "There is already a service running at " + appUrl)
        }
    
        val appJavaOpts =
          s"-Dspray.can.server.port=$port " +
          s"-Dmyapp.integrationTests.itMode=true " +
          s"-Dmyapp.externalServiceRootUrl=http://localhost:$port"
        val javaOptsName = if (isWindows) "JOPTS" else "JAVA_OPTS"
        val startFile = if (isWindows) "start.bat" else "start"
    
        // Launch the app, wait for it to come online
        val process: Process = Process(
          "./target/" + startFile,
          None,
          javaOptsName -> appJavaOpts)
            .run()
        processAndExitVal = (process, Future(process.exitValue()))
    
        // We add the port on which we launched the app to the System properties
        // for the current process.
        // The functional tests about to run in this process will notice this
        // when they load their config just before they try to connect to the app.
        System.setProperty("myapp.integrationTests.appPort", port)
    
        // poll until either the app has exited early or we can connect to the
        // app, or timeout
        waitUntilTrue(20.seconds) {
          if (processAndExitVal._2.isCompleted) {
            throw new IllegalStateException("The functional test target app has exited.")
          }
          canConnectTo(appUrl)
        }
      }
    
      /**
       * Forcibly terminates the process started in 'launchApp'
       */
      def shutdownApp(): Unit = {
        println("Closing the functional test target app")
        if (isWindows)
          shutdownAppOnWindows()
        else
          processAndExitVal._1.destroy()
      }
    
      /**
       * Java processes on Windows do not respond properly to
       * "destroy()", perhaps because they do not listen to WM_CLOSE messages
       *
       * Also there is no easy way to obtain their PID:
       * http://stackoverflow.com/questions/4750470/how-to-get-pid-of-process-ive-just-started-within-java-program
       * http://stackoverflow.com/questions/801609/java-processbuilder-process-destroy-not-killing-child-processes-in-winxp
       *
       * http://support.microsoft.com/kb/178893
       * http://stackoverflow.com/questions/14952948/kill-jvm-not-forcibly-from-command-line-in-windows-7
       */
      private def shutdownAppOnWindows(): Unit = {
        // Find the PID of the server process via netstat
        val netstat = "netstat -ano".!!
    
        val m = s"(?m)^  TCP    127.0.0.1:${port}.* (\\d+)$$".r.findFirstMatchIn(netstat)
    
        if (m.isEmpty) {
          println("FunctionalTestHelper: Unable to shut down app -- perhaps it did not start?")
        } else {
          val pid = m.get.group(1).toInt
          s"taskkill /f /pid $pid".!
        }
      }
    
      /**
       * True if a connection could be made to the given URL
       */
      def canConnectTo(url: URL): Boolean = {
        try {
          url.openConnection()
            .getInputStream()
            .close()
          true
        } catch {
          case _:Exception => false
        }
      }
    
      /**
       * Polls the given action until it returns true, or throws a TimeoutException
       * if it does not do so within 'timeout'
       */
      def waitUntilTrue(timeout: Duration)(action: => Boolean): Unit = {
        val startTimeMillis = System.currentTimeMillis()
        while (!action) {
          if ((System.currentTimeMillis() - startTimeMillis).millis > timeout) {
            throw new TimeoutException()
          }
        }
      }
    }
    

    【讨论】:

    • 传奇!这看起来棒极了
    • @Rich 你没有发现这种方法在你的每次测试之前都会启动和关闭服务器吗?这就是我在尝试复制您的方法时发现的情况,尽管可能我复制不正确。
    • 我上次使用它时没有,但那是前一段时间了。以下是 SBT 中 Test.Setup 的文档:scala-sbt.org/1.x/docs/Testing.html#Setup+and+Cleanup 它们实际上并没有说明它们是按测试还是按运行,但我认为它们打算成为后者。
    • 谢谢!我看过该文档,但每次都在上/下。我会再试一次,看看我能不能让它向上/向下一次!
    猜你喜欢
    • 1970-01-01
    • 2023-03-12
    • 2017-05-09
    • 2014-06-16
    • 2021-07-18
    • 1970-01-01
    • 1970-01-01
    • 2012-12-02
    • 2014-02-08
    相关资源
    最近更新 更多