我想在我的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”模式运行应用程序,而不是“默认”模式。 / p>
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进行设置,而是添加一个测试标志,将测试标记为“集成”并编写外部脚本来处理此问题。
答案 0 :(得分:13)
我现在已经编写了自己的代码来执行此操作。 我遇到的问题:
我发现将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()
}
}
}
}