49

问题

当我使用支持类型级编程的库时,我经常会发现自己在写如下注释(来自Paul Snively 在 Strange Loop 2012 中提出的示例):

// 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 这样的库的上下文中很有用,几天前我写了以下内容作为对这个问题的快速第一次尝试:

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]

我惊讶地发现这种类型级uniquefor 的实现HList不起作用,因为 Shapeless 很乐意FilterAux在后一种情况下找到一个实例。换句话说,即使您可能不希望它编译,以下内容也会编译:

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

在这种情况下,我看到的是一个错误——或者至少是一些类似错误的东西——并且它已经被修复了

更一般地说,我们可以想象想要检查隐含在我的期望中的那种不变量,它FilterAux 应该如何处理单元测试之类的东西——就像谈论测试这样的类型级代码听起来很奇怪,所有最近关于类型测试的相对优点的辩论。

我的问题

问题是我不知道任何类型的测试框架(适用于任何平台)允许程序员断言某些东西不能编译

我可以想象的一种方法是FilterAux使用旧的implicit-argument-with-null-default技巧

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 来说是理想的,但我会满足于任何东西。

4

5 回答 5

26

不是一个框架,但 Jorge Ortiz ( @JorgeO ) 提到了他在 2012 年在 NEScala 为 Foursquare 的 Rogue 库的测试中添加的一些实用程序,这些实用程序支持非编译测试:你可以在这里找到示例。很长一段时间以来,我一直想在无形中添加这样的东西。

最近,Roland Kuhn ( @rolandkuhn ) 添加了一个类似的机制,这次使用 Scala 2.10 的运行时编译来测试 Akka 类型化通道

当然,这些都是动态测试:如果不应该编译的东西发生了,它们会在(测试)运行时失败。无类型宏可能提供静态选项:即。宏可以接受无类型树,对其进行类型检查,如果成功则抛出类型错误)。这可能是在无形的宏观天堂分支上尝试的东西。但显然不是 2.10.0 或更早版本的解决方案。

更新

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

我现在使用 Jorge 的方法2.9.x 添加了 shapeless 实现,使用 Stefan 的方法为 2.10.x 添加了实现,使用无类型宏方法为宏天堂添加了实现。可以在此处找到 2.9.x此处为 2.10.x此处为宏天堂的相应测试示例。

无类型宏测试是最干净的,但 Stefan 的 2.10.x 兼容方法紧随其后。

于 2013-02-28T10:19:45.553 回答
22

ScalaTest 2.1.0 的Assertions语法如下:

assertTypeError("val s: String = 1")

对于Matchers

"val s: String = 1" shouldNot compile
于 2014-03-12T21:46:52.893 回答
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

它不适合您的问题(因为您没有根据断言指定测试用例),但可能是一种方法和/或给您一个良好的开端。

于 2013-02-28T10:52:03.353 回答
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]") 
}
于 2013-03-10T14:46:43.293 回答
4

compileErrorµTest 中的宏就是这样做的:

compileError("true * false")
// CompileError.Type("value * is not a member of Boolean")

compileError("(}")
// CompileError.Parse("')' expected but '}' found.")
于 2015-03-09T19:44:32.850 回答