测试一个断言,该断言指出某个内容不应该被编译。

49

问题

当我使用支持类型级编程的库时,我经常会写出像下面这样的注释(来自Paul Snively在2012年Strange Loop上的一个示例):

// But these invalid sequences don't compile:
// isValid(_3 :: _1 :: _5 :: _8 :: _8 :: _2 :: _8 :: _6 :: _5 :: HNil)
// isValid(_3 :: _4 :: _5 :: _8 :: _8 :: _2 :: _8 :: _6 :: HNil)

或者这个,来自 一个示例Shapeless 仓库中:

/**
 * If we wanted to confirm that the list uniquely contains `Foo` or any
 * subtype of `Foo`, we could first use `unifySubtypes` to upcast any
 * subtypes of `Foo` in the list to `Foo`.
 *
 * The following would not compile, for example:
 */
 //stuff.unifySubtypes[Foo].unique[Foo]

这只是表明这些方法行为的一种粗略方式,我们可以想象希望使这些断言更加正式,以进行单元或回归测试等。在类似Shapeless这样的库中给出一个具体的例子,几天前我写了以下内容作为快速回答this question的首次尝试:
import shapeless._

implicit class Uniqueable[L <: HList](l: L) {
  def unique[A](implicit ev: FilterAux[L, A, A :: HNil]) = ev(l).head
}

意图是这将编译:

('a' :: 'b :: HNil).unique[Char]

这不会:
('a' :: 'b' :: HNil).unique[Char]

我很惊讶地发现,这个针对HList的类型级别unique实现并没有起作用,因为Shapeless可以在后一种情况下轻松找到FilterAux实例。换句话说,即使你可能不希望如下代码编译,它也会通过:

implicitly[FilterAux[Char :: Char :: HNil, Char, Char :: HNil]]

在这种情况下,我看到的是一个bug,或者至少是类似bug的东西,而且已经被修复
更一般地说,我们可以想象希望检查类似于单元测试的不变性,这些不变性隐含在我的期望中,即FilterAux应该如何工作,尽管这听起来很奇怪,因为最近有关类型和测试相对优点的辩论。
我的问题是,我不知道是否有任何测试框架(适用于任何平台),允许程序员断言某些内容不能编译

对于FilterAux情况,我能想到的一种方法是使用旧的具有空默认值的隐式参数技巧

def assertNoInstanceOf[T](implicit instance: T = null) = assert(instance == null)

这将使您在单元测试中可以编写以下内容:
assertNoInstanceOf[FilterAux[Char :: Char :: HNil, Char, Char :: HNil]]

以下内容更加方便和表达自如,虽然有些麻烦:
assertDoesntCompile(('a' :: 'b' :: HNil).unique[Char])

我想要这个。我的问题是是否有人知道任何测试库或框架支持类似的东西 - 最好是针对Scala,但我会接受任何东西。


我假设您不想将上下文输入解释器实例并以此进行测试? - Rex Kerr
@RexKerr:不是手动的,没有。我很乐意编写一个采用这种方法的框架,而且我认为这并不是非常困难,但我更喜欢找到已经为我编写了它的人。 - Travis Brown
我听到了“Scala Types”(一种播客)的其中一位成员谈论他正在进行的工作,即验证编译器不会无意中接受无效的源代码。我想那个人是Yuvi Masory。如果你擅长搜索,这可能会出现在节目注释中... - Randall Schulz
5个回答

26
不是一个框架,但Jorge Ortiz(@JorgeO)在2012年NEScala上提到了他为Foursquare的Rogue库添加的一些测试工具,支持非编译测试:您可以在此处找到示例here。我一直想为shapeless添加类似的东西。

最近,Roland Kuhn(@rolandkuhn)添加了一个类似的机制,这次使用Scala 2.10的运行时编译,用于tests for Akka typed channels

这当然是动态测试:如果不应该编译的东西出现了,它们会在(测试)运行时失败。未经类型检查的宏可能提供静态选项:即一个宏可以接受一个未经类型检查的树,对其进行类型检查,并在成功时抛出类型错误。这可能是在shapeless的macro-paradise分支上进行实验的一些内容。但显然不适用于2.10.0或更早版本。

更新

自回答该问题以来,由Stefan Zeiger (@StefanZeiger)提出了另一种方法has surfaced。这个方法很有趣,因为像上面提到的未经类型检查的宏一样,它是编译时而不是(测试)运行时检查,但它也与Scala 2.10.x兼容。因此,我认为它比Roland的方法更可取。

我现在已经为shapeless添加了实现使用Jorge的方法实现2.9.x版本,使用Stefan的方法实现2.10.x版本以及使用未类型化宏方法实现宏天堂。相应测试的示例可以在此处找到:2.9.x这里, 2.10.x这里macro paradise这里
未标注类型的宏测试是最干净的,但 Stefan 的 2.10.x 兼容方法紧随其后。

5
在你最后一段的建议有效。如果你不想接受一堆用这种方式实现的测试的拉取请求来对topic/macro-paradise进行测试,最好现在就停止我。 :) - Travis Brown
@MilesSabin:这些中有哪些是在官方发布的地方可用的?我在shapeless_2.10:1.2.4中没有看到shapeless.test包。 - Steve
请密切关注即将在不久的将来发布的2.0.0里程碑版本。 - Miles Sabin

22

ScalaTest 2.1.0的断言语法如下:

assertTypeError("val s: String = 1")

对于Matchers

"val s: String = 1" shouldNot compile

这里是一个例子,当两个shouldshouldNot在同一情况下都不出错时。我做错了什么还是可能是个bug? - Sergio Pelin

9

你知道Scala项目中的partest吗?例如,CompilerTest有以下文档:

/** For testing compiler internals directly.
* Each source code string in "sources" will be compiled, and
* the check function will be called with the source code and the
* resulting CompilationUnit. The check implementation should
* test for what it wants to test and fail (via assert or other
* exception) if it is not happy.
*/

它能够检查例如此源代码https://github.com/scala/scala/blob/master/test/files/neg/divergent-implicit.scala是否会有此结果https://github.com/scala/scala/blob/master/test/files/neg/divergent-implicit.check。虽然您未指定测试用例的断言,但它不是完美匹配,但可能是一种方法和/或使您事半功倍的起点。

1
+1 和非常感谢 —— 我以前从未关注过 partest。不过,我认为 Miles 回答中的方法可能更适合我的目的。 - Travis Brown

6

根据 Miles Sabin 提供的链接,我能够使用 akka 版本。

import scala.tools.reflect.ToolBox

object TestUtils {

  def eval(code: String, compileOptions: String = "-cp target/classes"): Any = {
    val tb = mkToolbox(compileOptions)
    tb.eval(tb.parse(code))
  }

  def mkToolbox(compileOptions: String = ""): ToolBox[_ <: scala.reflect.api.Universe] = {
    val m = scala.reflect.runtime.currentMirror
    m.mkToolBox(options = compileOptions)
  }
}

然后在我的测试中,我像这样使用它。
def result = TestUtils.eval(
  """|import ee.ui.events.Event
     |import ee.ui.events.ReadOnlyEvent
     |     
     |val myObj = new {
     |  private val writableEvent = Event[Int]
     |  val event:ReadOnlyEvent[Int] = writableEvent
     |}
     |
     |// will not compile:
     |myObj.event.fire
     |""".stripMargin)

result must throwA[ToolBoxError].like {
  case e => 
    e.getMessage must contain("value fire is not a member of ee.ui.events.ReadOnlyEvent[Int]") 
}

4
在µTest中,compileError宏就是用来做这件事的:
compileError("true * false")
// CompileError.Type("value * is not a member of Boolean")

compileError("(}")
// CompileError.Parse("')' expected but '}' found.")

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接