2014-02-26 117 views
12

我想向我的SBT + Spray应用程序添加集成测试阶段。SBT集成测试设置

理想情况下是一样的Maven,有以下几个阶段:

  • compile:这个应用程序是建立
  • test:本机运行测试
  • pre-integration-test:这个应用程序是在推出单独的过程
  • integration-test:集成测试运行;他们发布在后台运行要求的应用程序,并确认正确的结果返回
  • post-integration-test:此前推出关机

我遇到了很多麻烦得到这个应用程序的实例工作。我可以遵循一个有效的例子吗?

1)分离的“它”基本代码:

我开始通过加入project/Build.scala"Integration Test" section of the SBT docs显示一个新的文件中的代码。

这使我可以在“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,以及单独的“它”的代码库有真糊涂了。它不能编译该目录中的任何代码,因为它认为所有的依赖关系都丢失了。

我想,而不是粘贴从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设置这个,而是增加一个测试标志来将测试标记为“集成”并编写一个外部脚本来处理这个问题。

回答

13

我现在写了自己的代码来做到这一点。我遇到 问题:

  • 我发现,将我的build.sbtproject/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() 
     } 
    } 
    } 
} 
+0

传奇!这看起来很棒 – Stephen