1. 测试 sbt 插件

测试 sbt 插件 

让我们谈谈测试。一旦你编写了插件,它就变成了一个长期的事情。为了不断添加新功能(或修复 bug),编写测试是有意义的。

scripted 测试框架 

sbt 带有一个 scripted 测试框架,允许你编写一个构建场景的脚本。它是用来测试 sbt 本身在复杂场景下的行为,比如变更检测和部分编译。

现在,假设你删除了 B.scala,但没有更新 A.scala。当你重新编译时,你应该会收到一个错误,因为 A 无法再引用 B。[...(非常复杂的东西)]

scripted 测试框架用于验证 sbt 能够处理像上面描述的这种场景。

该框架通过 scripted-plugin 提供。本页面的其余部分介绍如何将 scripted-plugin 包含到你的插件中。

步骤 1:快照 

在开始之前,将你的版本设置为 -SNAPSHOT 版本,因为 scripted-plugin 会将你的插件发布到本地。如果你不使用 SNAPSHOT,你可能会遇到一个糟糕的不一致状态,即你和其他用户看到不同的工件。

步骤 2:SbtPlugin 

build.sbt 中启用 SbtPlugin

lazy val root = (project in file("."))
  .enablePlugins(SbtPlugin)
  .settings(
    name := "sbt-something"
  )

然后将以下设置添加到 build.sbt

lazy val root = (project in file("."))
  .enablePlugins(SbtPlugin)
  .settings(
    name := "sbt-something",
    scriptedLaunchOpts := { scriptedLaunchOpts.value ++
      Seq("-Xmx1024M", "-Dplugin.version=" + version.value)
    },
    scriptedBufferLog := false
  )

注意:你必须使用 sbt 1.2.1 及更高版本才能使用 SbtPlugin

步骤 3:src/sbt-test 

创建目录结构 src/sbt-test/<test-group>/<test-name>。为了开始,尝试使用类似于 src/sbt-test/<your-plugin-name>/simple 的结构。

现在准备好了吗?在 simple 中创建一个初始构建。就像使用你的插件的真实构建一样。我相信你已经有一些手动测试的构建。以下是一个 build.sbt 示例。

lazy val root = (project in file("."))
  .settings(
    version := "0.1",
    scalaVersion := "2.10.6",
    assembly / assemblyJarName := "foo.jar"
  )

project/plugins.sbt

sys.props.get("plugin.version") match {
  case Some(x) => addSbtPlugin("com.eed3si9n" % "sbt-assembly" % x)
  case _ => sys.error("""|The system property 'plugin.version' is not defined.
                         |Specify this property using the scriptedLaunchOpts -D.""".stripMargin)
}

这是一个我从 earldouglas/xsbt-web-plugin@feabb2 学到的技巧,它允许我们将版本号传递到测试中。

我还有 src/main/scala/hello.scala

object Main {
  def main(args: Array[String]): Unit = {
    println("hello")
  }
}

步骤 4:编写脚本 

现在,编写一个脚本,在一个名为 test 的文件中描述你的场景,该文件位于你的测试项目的根目录。

# check if the file gets created
> assembly
$ exists target/scala-2.10/foo.jar

以下是脚本的语法

  1. # 开始单行注释
  2. > name 将任务发送到 sbt(并测试它是否成功)
  3. $ name arg* 执行文件命令(并测试它是否成功)
  4. -> name 将任务发送到 sbt,但预期会失败
  5. -$ name arg* 执行文件命令,但预期会失败

文件命令包括

  • touch path+ 创建或更新文件的最后修改时间
  • delete path+ 删除文件
  • exists path+ 检查文件是否存在
  • mkdir path+ 创建目录
  • absent path+ 检查文件是否不存在
  • newer source target 检查 source 是否比 target 更新
  • must-mirror source target 检查 source 是否与 target 相同
  • pause 暂停,直到按下回车键
  • sleep time 休眠(以毫秒为单位)
  • exec command args* 在另一个进程中运行命令
  • copy-file fromPath toPath 复制文件
  • copy fromPath+ toDir 将路径复制到 toDir,保留相对结构
  • copy-flat fromPath+ toDir 将路径复制到 toDir,扁平化结构

因此我的脚本将运行 assembly 任务,并检查 foo.jar 是否被创建。我们将在后面介绍更复杂的测试。

步骤 5:运行脚本 

要运行脚本,回到你的插件项目,并运行

> scripted

这将把你的测试构建复制到一个临时目录,并执行 test 脚本。如果一切顺利,你将看到 publishLocal 正在运行,然后

Running sbt-assembly / simple
[success] Total time: 18 s, completed Sep 17, 2011 3:00:58 AM

步骤 6:自定义断言 

文件命令很好,但还远远不够,因为它们没有测试实际内容。测试内容的一种简单方法是在你的测试构建中实现一个自定义任务。

对于我的 hello 项目,我想检查生成的 jar 是否打印出 "hello"。我可以利用 scala.sys.process.Process 来运行 jar。要表达失败,只需抛出错误。以下为 build.sbt

import scala.sys.process.Process

lazy val root = (project in file("."))
  .settings(
    version := "0.1",
    scalaVersion := "2.10.6",
    assembly / assemblyJarName := "foo.jar",
    TaskKey[Unit]("check") := {
      val process = Process("java", Seq("-jar", (crossTarget.value / "foo.jar").toString))
      val out = (process!!)
      if (out.trim != "bye") sys.error("unexpected output: " + out)
      ()
    }
  )

我故意测试它是否匹配 "bye",以查看测试是如何失败的。

以下是 test

# check if the file gets created
> assembly
$ exists target/foo.jar

# check if it says hello
> check

运行 scripted 按照预期失败了测试

[info] [error] {file:/private/var/folders/Ab/AbC1EFghIj4LMNOPqrStUV+++XX/-Tmp-/sbt_cdd1b3c4/simple/}default-0314bd/*:check: unexpected output: hello
[info] [error] Total time: 0 s, completed Sep 21, 2011 8:43:03 PM
[error] x sbt-assembly / simple
[error]    {line 6}  Command failed: check failed
[error] {file:/Users/foo/work/sbt-assembly/}default-373f46/*:scripted: sbt-assembly / simple failed
[error] Total time: 14 s, completed Sep 21, 2011 8:00:00 PM

步骤 7:测试测试 

在你习惯之前,可能需要一段时间才能让测试本身正确运行。有一些技巧可能会派上用场。

首先要做的就是关闭日志缓冲。

> set scriptedBufferLog := false

例如,这应该会打印出临时目录的位置

[info] [info] Set current project to default-c6500b (in build file:/private/var/folders/Ab/AbC1EFghIj4LMNOPqrStUV+++XX/-Tmp-/sbt_8d950687/simple/project/plugins/)
...

将以下行添加到你的 test 脚本中,以暂停测试,直到你按下回车键

$ pause

如果你正在考虑进入 sbt/sbt-test/sbt-foo/simple 并运行 sbt,请不要这样做。正确的方法是将目录复制到其他地方并运行它。

步骤 8:获得灵感 

在 sbt 项目本身中,有 100 多个 scripted 测试。浏览它们以获取灵感。

例如,这里有一个名为 by-name 的测试。

> compile

# change => Int to Function0
$ copy-file changes/A.scala A.scala

# Both A.scala and B.scala need to be recompiled because the type has changed
-> compile

xsbt-web-pluginsbt-assembly 也有一些 scripted 测试。

就这样!请告诉我你测试插件的经验!