抽取器、断言与测试
抽取器(Extractors)
例子:抽取email地址
对于一个email类的话,如果是合法的email地址取出用户名与域名。
EMail(user, domain)
模式匹配表达式可以写为:
s match { case EMail(user, domain) => println(user +" AT "+ domain) case _ => println("not an email address") }
找到两个连续的同一用户的email地址的模式:
ss match { case EMail(u1, d1) :: EMail(u2, d2) :: _ if (u1 == u2) => ... ... }
现在要匹配的email不是一个类,而是字符串。我们先把方法定义出来:
def isEMail(s: String): Boolean def domain(s: String): String def user(s: String): String
调用的时候就是这样的:
if (isEMail(s)) println(user(s) +" AT "+ domain(s) else println("not an email address")
抽取器
在scala对象中与apply
方法相对的方法是unapply
方法,而有unapply
成员方法的对象
就是抽取器。unapply
是为了匹配并分解值。
下面的例子中apply
方法注入对象,而unapply
方法从对象中抽取内容:
object EMail { // The injection method (optional) def apply(user: String, domain: String) = user +"@"+ domain // The extraction method (mandatory) def unapply(str: String): Option[(String, String)] = { val parts = str split "@" if (parts.length == 2) Some(parts(0), parts(1)) else None } }
还可以让这个对象继承自Scala的函数类型:
object EMail extends (String, String) => String { ... }
对象声明里的(String, String) => String
的意思相当于Function2[String, String]
,是对Email类实现的抽象apply
方法的声明。这样可以把Email传递给需要
Function2[String,String]
的方法。
unapply返回类型为Option
注意unapply
方法返回类型是Option
。因为输入参数可能不是正确的email格式。
unapply("John@epfl.ch") equals Some("John", "epfl.ch") unapply("John Doe") equals None
unapply与模式匹配
现在,当模式匹配到抽取器对象指定的模式就会在选择器表达式中调用抽取器的unapply
方法。如下面的代码:
selectorString match { case EMail(user, domain) => ... }
String
类型的selectorString
其实先被抽取器处理:
EMail.unapply(selectorString)
产的结构再进行模式匹配判断。
selectorString
的类型虽然和unapply
一样都是String
,但这并不是必须的。像下面
这样检查任意类型的实例是不是email:
val x: Any = ... x match { case EMail(user, domain) => ... }
apply与unapply应该有对偶关系
一般来说如果包含了注入方法,那应该与抽取方法成对偶关系。如调用:
EMail.unapply(EMail.apply(user, domain))
应该返回:
Some(user, domain)
也就是说被Some
包装的同一序列的参数。反过来就是先执行unapply
再执行apply
:
EMail.unapply(obj) match { case Some(u, d) => EMail.apply(u, d) }
虽然这样的对偶性不是强制要求的,但强烈建议实现。
只有1个或没有变量的模式
之前unapply
方法返回的是元组,这在有多个值要返回的时候很有用。但是在只有一个值
或没有值要返回的时候有麻烦,因为元组最小是二元组,没有有一元元组。
所以模式只绑定一个变量的情况要特别对待,把结果直接放在Some
中:
object Twice { def apply(s: String): String = s + s def unapply(s: String): Option[String] = { val length = s.length / 2 val half = s.substring(0, length) if (half == s.substring(length)) Some(half) else None } }
还有一种情况下抽取器模式不绑定任何变量。这样情况下返回布尔值表示匹配成功或失败:
object UpperCase { def unapply(s: String): Boolean = s.toUpperCase == s }
注意上面的代码没有apply
,因为本来就没有什么好构造的。
下面的userTwiceUpper
函数的模式匹配代码集中了前面定义的所有抽取器:
def userTwiceUpper(s: String) = s match { case EMail(Twice(x @ UpperCase()), domain) => "match: "+ x +" in domain "+ domain case _ => "no match" }
函数的第一部分匹配email地址,并且用户名部分需要由大家字母形式的相同字符串出现 两次组成。
注意第二行里的UpperCase
的空参数列表()
是不能省略的,不然会被解释为与
UpperCase
对象进行匹配。还要注意虽然UpperCase()
本身没有绑定任何变量,但还可以
把变量与匹配它的整个模式联系起来。用模式中的变量绑定方案;以x @ UpperCase()
的
模式把x
与UpperCase()
匹配的模式联系起来。例如在第一个userTwiceUpper
调用中,
x
被绑定为DI
,因为匹配于UpperCase()
模式的值。
scala> userTwiceUpper("DIDI@hotmail.com") res0: java.lang.String = match: DI in domain hotmail.com scala> userTwiceUpper("DIDO@hotmail.com") res1: java.lang.String = no match scala> userTwiceUpper("didi@hotmail.com") res2: java.lang.String = no match
可变参数的抽取器
希望的元素个数是可变的,如对域名的处理:
dom match { case Domain("org", "acm") => println("acm.org") case Domain("com", "sun", "java") => println("java.sun.com") case Domain("net", _*) => println("a .net domain") }
可以看到上面的域名是反向展开的,最后一个情况下下_*
剩下的所有元素。unapply
值
的返回个数定下了就不能改了,所以Scala允许为变参数定义不同的抽取方法unapplySeq
:
object Domain { // The injection method (optional) def apply(parts: String*): String = parts.reverse.mkString(".") // The extraction method (mandatory) def unapplySeq(whole: String): Option[Seq[String]] = Some(whole.split("\\.").reverse) }
unapplySeq
以句点拆分字符串包装在Some
中返回。unapplySeq
抽取器返回结果类型
必须是Option[seq[T]]
,这里的元素类型T
不能限制。Seq
是各种序列类(List、
Array、RichString等)的共同超类。
这样寻找某个.com
域名中的email的函数就是这样:
def isTomInDotCom(s: String): Boolean = s match { case EMail("tom", Domain("com", _*)) => true case _ => false }
返回期望的结果:
scala> isTomInDotCom("tom@sun.com") res3: Boolean = true scala> isTomInDotCom("peter@sun.com") res4: Boolean = false scala> isTomInDotCom("tom@acm.org") res5: Boolean = false
同样也可以从unapplySeq
及变化部分返回固定的元素。表达为包含所有元素的元组,变化
部分还是在最后,下面是新的抽取器,其中域名部分已经扩展为序列了:
object ExpandedEMail { def unapplySeq(email: String) : Option[(String, Seq[String])] = { val parts = email split "@" if (parts.length == 2) Some(parts(0), parts(1).split("\\.").reverse) else None } }
这里unapplySeq
方法返回对偶的可选类型。对偶的第一个元素是用户部分,第二个部分是
表示域名的序列。然后就可以这样匹配它:
scala> val s = "tom@support.epfl.ch" s: java.lang.String = [[tom@support.epfl.ch]] scala> val ExpandedEMail(name, topdom, subdoms @ _*) = s name: String = tom topdom: String = ch subdoms: Seq[String] = List(epfl, support)
抽取器和序列模式
模式匹配中我们已经知道可以使用序列模式访问列表或数组的元素:
List() List(x, y, _*) Array(x, 0, 0, _)
这里的List(...)
形式的模式其实是由scala.List
的伴生对象定义了unapplySeq
方法
的抽取器,相关定义如下:
package scala object List { def apply[T](elems: T*) = elems.toList def unapplySeq[T](x: List[T]): Option[Seq[T]] = Some(x) ... }
List对象包含了带可变数量参数的apply
方法,从而允许编写如下的表达式:
List() List(1, 2, 3)
它还包含了以序列形式返回列表所有元素的unapplySeq
方法,从而对List(...)
模式
提供了支持。scala.Array
对象定义也非常类似,所以数组也支持注入和抽取方法。
抽取器与模式匹配
抽取器中置模式匹配
如果unapply
方法产出的是一个对偶,就可以在模式匹配case
语句中使用中置表示法。
对于有两个参数的样本类:
case class Currency(value: Double, unit: String) val amt = Currency(100.00, "EUR")
下面的匹配语句:
amt match { case Currency(a, u) => println(u + " " + a) }
可用中置表达格式:
amt match { case a Currency u => println(u + " " + a) }
中置表示法可以用于任何返回对偶的unapply
方法,比如:
case object +: { def unapply[T](input: List[T]) = if (input.isEmpty) None else Some((input.head, input.tail)) }
这样就可以直接用+:
来析构列表了:
1 +: 7 +: 2 +:9 +: Nil match { case first +: second +: rest => first + second + rest.length }
抽取器 VS. 样本类
样本类会暴露了数据的具体表达方式,让外部看到了类名与构造器等信息,如:
case C(...)
对于已经存在的样本类后来写的代码一定要拿来用它们。如果修改了样本类就一定要把用到 的地方都一起改了。抽取器是独立的,没有这个问题。
样本类的优点是容易实现、性能更加高效。而且样本类如果继承自sealed
,编译器可以
穷举所有可能性检查程序里会漏掉的逻辑。
一般来说,如果是封闭的应用,样本类更加合适;如果是开放的,类层级之类的会有重构, 就更加适合抽取器。
在面临选择的时候可以先从样本类开始做,以后发觉有问题再换抽取器。因为抽取器与 样本类模式在Scala上看上去样子完全一样,所以在客户代码中的模式匹配还是可以继续 工作的。但是像本章的email例子中模式架构与数据表现类不相符的情况下,只能用抽取器 。
正则表达式
正则表达式规则看java.util.regex.Pattern
包的JavaDoc。RTFM!
简单的情况下不用新建正则表达式类,直接用String自带的成员方法就可以了。
String类的matches
方法检查字符串是否匹配指定模式:
scala> "Foroggy went a' counting" matches ".* counting" res8: Boolean = true
String类的replaceAll
方法替换所有匹配的模式:
scala> "milk, tea, muck" replaceAll ("m[^ ]+k", "coffee") res9: String = coffee, tea, coffee
String类的replaceFirst
方法替换第一个匹配的模式:
scala> "milk, tea, muck" replaceFirst ("m[^ ]+k", "coffee") res10: String = coffee, tea, muck
形成正则表达式
Scala相关正则的类放在scala.util.matching
包里:
scala> import scala.util.matching.Regex
通过Regex
类构造器传递正则表达式:
scala> val Decimal = new Regex("(-)?(\\d+)(\\.\\d*)?") Decimal: scala.util.matching.Regex = (-)?(\d+)(\.\d*)?
Scala的照排字符串可以免了java复杂的转义:
scala> val Decimal = new Regex("""(-)?(\d+)(\.\d*)?""") Decimal: scala.util.matching.Regex = (-)?(\d+)(\.\d*)?
还可以更加简化:
scala> val Decimal = """(-)?(\d+)(\.\d*)?""".r Decimal: scala.util.matching.Regex = (-)?(\d+)(\.\d*)?
RichString
类里实现了r
方法把字符串转为正则表达式:
package scala.runtime import scala.util.matching.Regex class RichString(self: String) ... { ... def r = new Regex(self) }
变量插值会影响转义
使用三个引号的原文字符串可以不用转义反斜杠\
:
scala> for(s <- """\w+\s\w+""".r.findAllIn("Hello world!!!")) println(s) Hello world
但是如果使用了含有变量名的插值字符串形式,插值字符串中的反斜杠还是要转义的:
scala> var str = """\w+""" str: String = \w+ scala> for(s <- s"""$str\s$str""".r.findAllIn("Hello world!!!")) println(s) scala.StringContext$InvalidEscapeException: invalid escape '\s' not one of [\b, \t, \n, \f, \r, \\, \", \'] at index 0 in "\s". Use \\ for literal \. at scala.StringContext$.loop$1(StringContext.scala:236) at scala.StringContext$.replace$1(StringContext.scala:246) at scala.StringContext$.treatEscapes0(StringContext.scala:250) at scala.StringContext$.treatEscapes(StringContext.scala:195) at scala.StringContext$$anonfun$s$1.apply(StringContext.scala:95) at scala.StringContext$$anonfun$s$1.apply(StringContext.scala:95) at scala.StringContext.standardInterpolator(StringContext.scala:126) at scala.StringContext.s(StringContext.scala:95) ... 33 elided scala> for(s <- s"""$str\\s$str""".r.findAllIn("Hello world!!!")) println(s) Hello world
用正则表达式查找替换
查找首次出现作为Option
类型结果返回:
regex findFirstIn str
所有的出现的匹配以Iterator
类型结果返回:
regex findAllIn str
查找开始位置匹配出现,返回Option
类型:
regex findPrefixOf str
例子:
scala> val Decimal = """(-)?(\d+)(\.\d*)?""".r Decimal: scala.util.matching.Regex = (-)?(\d+)(\.\d*)? scala> val input = "for -1.0 to 99 by 3" input: java.lang.String = for -1.0 to 99 by 3 scala> for (s <- Decimal findAllIn input) | println(s) -1.0 99 3 scala> Decimal findFirstIn input res1: Option[String] = Some(-1.0) scala> Decimal findPrefixOf input res2: Option[String] = None
正则表达式抽取值
Scala所有的正则表达式都定义了抽取器,可以用来鉴别匹配于正则表达式分组的子字符串 ,相当于买一送一。
形式为:
val <regex>(<identifier>) = <input string>
例如:
scala> val ptn = """.* price is ([\d.]+) .*""".r ptn: scala.util.matching.Regex = .* price is ([\d.]+) .* scala> val ptn(price) = "He told me the price is 15.33 today." price: String = 15.33 scala> val ptn(priceStr) = "He told me the price is 15.33 today." priceStr: String = 15.33 scala> val price = priceStr.toDouble price: Double = 15.33
如果匹配有模式有多个部分,可以解构到多个变量中:
scala> val Decimal = """(-)?(\d+)(\.\d*)?""".r Decimal: scala.util.matching.Regex = (-)?(\d+)(\.\d*)? scala> val Decimal(sign, integerpart, decimalpart) = "-1.23" sign: String = - integerpart: String = 1 decimalpart: String = .23
注意这里用Decimal(...)
来定义了一个val
。Decimal
正则表达式值定义了
unapplySeq
方法把字符串与正则匹配到三个模式变量sign
、intergerpart
、
decimalpart
,如果有一个部分缺少就是null
:
scala> val Decimal(sign, integerpart, decimalpart) = "1.0" sign: String = null integerpart: String = 1 decimalpart: String = .0
还可以在for表达式中混用抽取器与正则表达式做搜索,如,下面的代码从字符串中找到 所有的数值:
scala> for (Decimal(s, i, d) <- Decimal findAllIn input) | println("sign: "+ s +", integer: "+ | i +", decimal: "+ d) sign: -, integer: 1, decimal: .0 sign: null, integer: 99, decimal: null sign: null, integer: 3, decimal: null
断言
assert
是预定义的方法,assert(condition)
会在条件不成立时抛出AssertionError
。
另一个版本assert(condition, explanation)
,指定了抛出含有指定explantion
作为
说明的assertionError
。explation
的类型为Any
,所以任何对象都可以作为参数。
assert
方法会对传入的参数调用toString
的结果作为放在AssertionError
中的文字描述。
在前面的「组合与继承」这一章中的above
方法加上道检查,只有宽度一样的元素才可以
上下连接在一起:
def above(that: Element): Element = { val this1 = this widen that.width val that1 = that widen this.width elem(this1.contents ++ that1.contents) }
assert
方法所在的Predef
包里还有一个ensuring
方法可以直接对表达式的返回结果
进行测试而不用先把表达式的结果先存放到变量中:
def widen(w: Int): Element = if (w <= width) this else { val left = elem(' ', (w - width) / 2, height) var right = elem(' ', w - width - left.width, height) left beside this beside right } ensuring (w <= _.width)
ensuring
接收一个返回类型为Boolean的函数作为参数。注意这里ensuring
是作用在
if-else表达式的Element
类结果上的,而不是方法widen
的返回值上。相当于是:
def widen(w: Int): Element = { if (w <= width) this else { val left = elem(' ', (w - width) / 2, height) var right = elem(' ', w - width - left.width, height) left beside this beside right } ensuring (w <= _.width) }
还有一点要明白的是,这里的语法看起来像是对Element
类对象调用ensuring
方法(把
xxx.ensuring(...)
中的点换成了空格)。但是实际上Emement
对象没有这个成员方法,
而是被隐式转换成了Ensuring
对象。由于存在隐式转换,所以ensuring
可以作用在任何
类型上。
测试
受益于和Java之间无缝操作。Java的测试库,像JUnit、TestNG,可以直接用于scala的 单元测试。
Junit 4
如果喜欢JUnit4的风格,那么可以写下面这样的单元测试。
import java.util.ArrayList import org.junit.Test import org.junit.Assert._ class SampleTest { @Test def listAdd() { val list = new ArrayList[String] list add "milk" list add "sugar" assertEquals(2, list.size()) } }
产生的测试类可以直接在Java中使用:
$ scalac -classpath .:junit-4.10.jar tmp.scala $ java -classpath .:junit-4.10.jar:scala-library-2.10.2.jar \ org.junit.runner.JUnitCore SampleTest JUnit version 4.10 . Time: 0.009 OK (1 test)
ScalaTest
ScalaTest是专为scala设计的一个测试框架。ScalaTest提供了一些比较方便的功能。注意 ScalaTest除了自己的版本号外,对应不同的Scala版本还有不同的对应版本。拿错了是 用不了的。
先用脚本来测试一下对不对:
class CanaryTest extends org.scalatest.Suite { def testOK() { assert(true) } } (new CanaryTest).execute()
因为使用的是脚本,所以上面最后一行调用execute()
方法直接运行。执行:
$ scala -classpath .:scalatest_2.10-1.9.1.jar tmp.scala Main$$anon$1$CanaryTest: - testOK
Runner
ScalaTest的Runner
类来配置运行或不运行哪些套件,配置不同的reporter
来指定报表
。可以在ScalaTest的文档中查看所有的选项。
class CanaryTest extends org.scalatest.Suite { def testListEmpty() { val list = new java.util.ArrayList[Integer] assert(0 == list.size) } def testListAdd() { val list = new java.util.ArrayList[Integer] list add 1 list add 4 assert(2 == list.size) } }
通过Runner
打开图形界面,在view
菜单中可以显示测试过程信息:
$ scalac -classpath .:scalatest_2.10-1.9.1.jar tmp.scala $ scala -classpath .:scalatest_2.10-1.9.1.jar org.scalatest.tools.Runner -p . WARNING: -p has been deprecated and will be reused for a different (but still very cool) purpose in ScalaTest 2.0. Please change all uses of -p to -R.
上面的参数-p
指定的查找测试类的路径。
如果不想要图形界面,可以用-o
重定向结果到标准输出:
$ scala -classpath .:scalatest_2.10-1.9.1.jar org.scalatest.tools.Runner -p . -o WARNING: -p has been deprecated and will be reused for a different (but still very cool) purpose in ScalaTest 2.0. Please change all uses of -p to -R. Run starting. Expected test count is: 2 DiscoverySuite: CanaryTest: - testListAdd - testListEmpty Run completed in 150 milliseconds. Total number of tests run: 2 Suites: completed 2, aborted 0 Tests: succeeded 2, failed 0, ignored 0, pending 0 All tests passed.
还有-f
参数可以把结果输出到文件。
assert()与expect()方法
ScalaTest提供的assert()
方法测试条件是否成立。还有一个两个参数版本的,第二个
参数设置失败时的提示信息。
assert(2 == list.size(), "Unexpected size of list")
还有一个expect()
方法作为类似于Junit的assertEquals()
方法。注意它的第三个参数
是一个闭包:
assert(2, "Unexpected size of list") { list.size() }
注意expect()
方法已经过期,用expectResult()
方法代替。
测试异常
有一个比较啰嗦的方案:
try { // ..... fail("Expected exception here") } catch { case e: IndexOutOfBoundsException => // success }
更好的方法是用ScalaTest提供的intercept()
方法:
intercept(classOf[IndexOutOfBoundsException], "Expected exception here") { // ..... }
注意上面的格式可能会在ScalaTest的新版本中改变,会是这样的:
intercept[IndexOutOfBoundsException]("Expected exception here") { // ..... }
intercept()
方法会把捕获的结果作为返回值,如果需要的话可以对它进行处理。
在测试间共享代码
BeforeAndAfter特质
Scala的BeforeAndAfter
特质提供了beforeEach()
与afterEach()
方法会在每个测试
开始与结束前调用;还有beforeAll()
和afterAll()
在所有的开始与结束时运行一次。
class ShareCodeImperation extends org.scalatest.Suite with org.scalatest.BeforeAndAfter { var list: java.util.ArrayList[Integer] = _ override def beforeEach() { list = new java.util.ArrayList[Integer] } override def afterEach() { list = null } def testListEmptyOnCreate() { expect(0, "Expected size to be 0") { list.size() } } def testGetOnEmptyList() { intercept[IndexOutOfBoundsException] { list.get(0) } } } (new ShareCodeImperative).execute()
通过闭包
import org.scalatest.Suite import java.util.ArrayList class ShareCodeFunctional extends Suite { def withList(testFunction : (ArrayList[Integer]) => Unit) { // init list val list = new ArrayList[Integer] try { testFunction(list) } finally { // clean up } } def testListEmptyOnCreate() { withList { list => expectResult(0, "Expected size to be 0") { list.size() } } } def testGetOnEmptyList() { withList { list => intercept[IndexOutOfBoundsException] { list.get(0) } } } } (new ShareCodeFunctional).execute()
运行:
$ scala -deprecation -classpath .:scalatest_2.10-1.9.1.jar tmp.scala Main$$anon$1$ShareCodeFunctional: - testGetOnEmptyList - testListEmptyOnCreate
FunSuite
ScalaTest提供了函数式的FunSuite
(Function Suite)。test
方法为测试方法,说明
字符串的内容会显示在输出信息中:
import org.scalatest.FunSuite import scala.collection.mutable.Stack class ExampleSuite extends FunSuite { test("pop is invoked on a non-empty stack") { val stack = new Stack[Int] stack.push(1) stack.push(2) val oldSize = stack.size val result = stack.pop() assert(result === 2) assert(stack.size === oldSize - 1) } test("pop is invoked on an empty stack") { val emptyStack = new Stack[Int] intercept[NoSuchElementException] { emptyStack.pop() } assert(emptyStack.isEmpty) } }
\-(morgan:%) >>> scalac -cp scalatest_2.9.0-1.9.1.jar ExampleSuite.scala \-(morgan:%) >>> scala -cp .:scalatest_2.9.0-1.9.1.jar org.scalatest.run ExampleSuite Run starting. Expected test count is: 2 ExampleSuite: - pop is invoked on a non-empty stack - pop is invoked on an empty stack Run completed in 158 milliseconds. Total number of tests run: 2 Suites: completed 1, aborted 0 Tests: succeeded 2, failed 0, ignored 0, pending 0 All tests passed.
ScalaTest中也可以写JUnit风格的测试:
import org.scalatest.junit.AssertionsForJUnit import scala.collection.mutable.ListBuffer import org.junit.Assert._ import org.junit.Test import org.junit.Before class ExampleSuite extends AssertionsForJUnit { var sb: StringBuilder = _ var lb: ListBuffer[String] = _ @Before def initialize() { sb = new StringBuilder("ScalaTest is ") lb = new ListBuffer[String] } @Test def verifyEasy() { // Uses JUnit-style assertions sb.append("easy!") assertEquals("ScalaTest is easy!", sb.toString) assertTrue(lb.isEmpty) lb += "sweet" try { "verbose".charAt(-1) fail() } catch { case e: StringIndexOutOfBoundsException => // Expected } } @Test def verifyFun() { // Uses ScalaTest assertions sb.append("fun!") assert(sb.toString === "ScalaTest is fun!") assert(lb.isEmpty) lb += "sweeter" intercept[StringIndexOutOfBoundsException] { "concise".charAt(-1) } } }
编译运行:
\-(morgan:%) >>> scalac -cp scalatest_2.9.0-1.9.1.jar:junit-4.8.2.jar ExampleSuite.scala \-(morgan:%) >>> scala -cp .:scalatest_2.9.0-1.9.1.jar:junit-4.8.2.jar org.junit.runner.JUnitCore ExampleSuite JUnit version 4.8.2 .. Time: 0.026 OK (2 tests)
混合ScalaTest与JUnit
org.scalatest.junit.JUnitSuite
已经混入了特质AssertionsForJUnit
,可以同时被
用于JUnit与ScalaTest方法:
import org.scalatest.junit.JUnitSuite import scala.collection.mutable.ListBuffer import org.junit.Assert._ import org.junit.Test import org.junit.Before class ExampleSuite extends JUnitSuite { var sb: StringBuilder = _ var lb: ListBuffer[String] = _ @Before def initialize() { sb = new StringBuilder("ScalaTest is ") lb = new ListBuffer[String] } @Test def verifyEasy() { // Uses JUnit-style assertions sb.append("easy!") assertEquals("ScalaTest is easy!", sb.toString) assertTrue(lb.isEmpty) lb += "sweet" try { "verbose".charAt(-1) fail() } catch { case e: StringIndexOutOfBoundsException => // Expected } } @Test def verifyFun() { // Uses ScalaTest assertions sb.append("fun!") assert(sb.toString === "ScalaTest is fun!") assert(lb.isEmpty) lb += "sweeter" intercept[StringIndexOutOfBoundsException] { "concise".charAt(-1) } } }
JUnit调用:
\-(morgan:%) >>> scala -cp .:scalatest_2.9.0-1.9.1.jar:junit-4.8.2.jar org.junit.runner.JUnitCore ExampleSuite JUnit version 4.8.2 .. Time: 0.026 OK (2 tests)
ScalaTest调用:
\-(morgan:%) >>> scala -cp .:scalatest_2.9.0-1.9.1.jar:junit-4.8.2.jar org.scalatest.run ExampleSuite Run starting. Expected test count is: 2 ExampleSuite: - verifyEasy - verifyFun Run completed in 226 milliseconds. Total number of tests run: 2 Suites: completed 1, aborted 0 Tests: succeeded 2, failed 0, ignored 0, pending 0 All tests passed.
ScalaTest还提供了函数式的单元测试。
// 用函数式的方式来写单元测试 // IDE目前对ScalaTest的支持不是特别好 // 加上RunWith就可以用JUnit的方式来运行了 @RunWith(classOf[JUnitRunner]) class ElementSuite3 extends FunSuite { test("elem result should have passed width") { val ele = elem('x', 2, 3) assert(ele.width == 2) } }
单元测试对提高软件质量很有好处。唯一的不足就是只针对程序员。其它人员要看懂还是 比较困难。ScalaTest提供了BDD(Behavior Driven Development行为驱动开发)测试方式 。下面的这段测试代码在运行时就会打印出可读的解释。
class ElementSpec extends FlatSpec with ShouldMatchers { "A UniformElement" should "have a width equal to the passed value" in { val ele = elem('x', 2, 3) ele.width should be(2) } it should "have a height equal to the passed value" in { val ele = elem('x', 2, 3) ele.height should be(3) } it should "throw an IAE if passed a negative width" in { evaluating { elem('x', -2, 3) } should produce[IllegalArgumentException] } }
上面的代码会打印出下面这样的提示。
A UniformElement - should have a width equal to the passed value - should have a height equal to the passed value - should throw an IAE if passed a negative width
我们写单元测试时会测试一些边界值。然后再选一些典型的值。如果这些选值有库来做, 不但可以减少单元测试的工作量,而且可以将边界值选取更合理。
下面是如何将ScalaChecker
和ScalaTest
联合起来使用的一个例子:
class ElementSpecChecker extends FlatSpec with ShouldMatchers with Checkers{ "A UniformElement" should "have a width equal to the passed value" in { // 这可以用数学化的方式来读 // 对每个整数w // 当w>0时 // 都有后面的等式成立 check((w: Int) => w > 0 ==> (elem('x', w, 3).width == w)) } }