SBT集成测试设置 [英] SBT integration test setup

查看:71
本文介绍了SBT集成测试设置的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

要使其正常工作,我遇到很多麻烦.有没有可以效仿的例子?

1)单独的"it"代码库:

我首先添加了将SBT文档的集成测试"部分添加到位于project/Build.scala的新文件.

这使我可以在"src/it/scala"下添加一些集成测试,并使用"sbt it:test"运行它们,但是我看不到如何添加pre-integration-test挂钩.

问题"确保重新启动"任务在运行之前自动运行it:test 似乎解决了如何设置此类钩子的问题,但答案对我不起作用(请参阅

2)在测试"代码库中进行集成测试:

我正在使用IntelliJ,而单独的"it"代码库确实使它感到困惑.它认为缺少所有依赖项,因此无法编译该目录中的任何代码.

我尝试改为粘贴"具有共享源的其他测试配置",但出现编译错误:

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

有没有可以效仿的例子?

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

解决方案

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

  • 我发现将我的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()
      }
    }
  }
}

I would like to add an Integration Test phase to my SBT + Spray app.

Ideally it would be just like Maven, with the following phases:

  • compile: The app is built
  • test: The unit tests are run
  • pre-integration-test: The app is launched in a separate process
  • integration-test: The integration tests are run; they issue requests to the app running in the background and verify that the correct results are returned
  • post-integration-test: The instance of the app previously launched is shut down

I'm having a lot of trouble getting this to work. Is there a worked example that I can follow?

1) Separate "it" codebase:

I started by adding the code shown in the "Integration Test" section of the SBT docs to a new file at project/Build.scala.

This allowed me to add some integration tests under "src/it/scala" and to run them with "sbt it:test", but I can't see how to add a pre-integration-test hook.

The question "Ensure 're-start' task automatically runs before it:test" seems to address how to set up such a hook, but the answer doesn't work for me (see my comment on there).

Also, adding the above code to my build.scala has stopped the "sbt re-start" task from working at all: it tries to run the app in "it" mode, instead of in "default" mode.

2) Integration tests in "test" codebase:

I am using IntelliJ, and the separate "it" codebase has really confused it. It can't compile any of the code in that dir, as it thinks that all the dependencies are missing.

I tried to paste instead the code from "Additional test configurations with shared sources" from the SBT docs, but I get a compile error:

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

Is there a worked example I can follow?

I'm considering giving up on setting this up via SBT and instead adding a test flag to mark tests as "integration" and writing an external script to handle this.

解决方案

I have now written my own code to do this. Issues that I encountered:

  • I found that converting my build.sbt to a project/Build.scala file fixed most of the compile errors (and made compile errors in general much easier to fix, as IntelliJ could help much more easily).

  • The nicest way I could find for launching the app in a background process was to use sbt-start-script and to call that script in a new process.

  • Killing the background process was very difficult on Windows.

The relevant code from my app is posted below, as I think a few people have had this problem. If anyone writes an sbt plugin to do this "properly", I would love to hear of it.

Relevant code from 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)
}

This helper code is in 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()
      }
    }
  }
}

这篇关于SBT集成测试设置的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆