1. 概述
ScalaTest 是 Scala 生态中最流行、功能最完整且易于使用的测试框架之一。
与其他测试工具相比,ScalaTest 的独特之处在于它开箱即支持多种测试风格,比如 XUnit 和 BDD(行为驱动开发)。
本入门教程将从创建第一个测试开始,逐步介绍其对 XUnit 和 BDD 的支持。我们还将探索一些其他关键特性,如匹配器(Matchers)和模拟(Mocking),并展示如何写出简洁易懂的测试代码。
2. 依赖配置
配置 ScalaTest 非常简单,只需在 build.sbt
文件中添加如下依赖:
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % "test"
如果你使用的是 Maven 项目,可以在 pom.xml
中添加以下依赖:
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_3</artifactId>
<version>3.2.19</version>
<scope>test</scope>
</dependency>
你也可以从 Maven Central 获取最新版本。
3. 核心概念
在深入示例之前,先了解几个 ScalaTest 的核心概念:
- ✅ ScalaTest 的核心是 Suite(测试套件),它是一组零个或多个测试的集合。
- ✅
Suite
trait 声明了多个生命周期方法,定义了如何编写和运行测试。 - ✅ ScalaTest 提供了多种风格的 trait(如
AnyFunSuite
、AnyFlatSpec
),扩展自Suite
,支持不同测试风格。 - ✅ 我们可以将这些风格 trait 与 ScalaTest 提供的 stackable traits(如
Matchers
、BeforeAndAfter
)组合使用。 - ✅ 通过这种方式,我们可以快速构建出结构清晰、可读性强的测试。
- ✅ 当然,我们也可以在 ScalaTest 之外扩展这些 trait。
4. 编写第一个单元测试
配置好 ScalaTest 后,我们来编写一个简单的测试,用于测试 List
的行为:
class ListFunSuite extends AnyFunSuite {
test("An empty List should have size 0") {
assert(List.empty.size == 0)
}
}
这里,我们的测试类继承了 AnyFunSuite
trait。✅ 这个 trait 面向熟悉 XUnit 的开发者,允许我们将测试组织为带有描述性名称的 test()
块。
在上面的例子中,我们断言一个空的 List
大小为 0。使用 sbt
运行该测试:
sbt:scala-tutorials> testOnly com.baeldung.scala.scalatest.ListFunSuite
...
[info] Done compiling.
[info] ListFunSuite:
[info] - An empty List should have size 0
[info] ScalaTest
[info] Run completed in 87 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 1 s, completed Mar 25, 2020 4:32:17 PM
不出所料,测试通过了,sbt
输出了测试报告。
我们也可以添加更复杂的测试,例如检查是否抛出预期异常:
test("Accessing invalid index should throw IndexOutOfBoundsException") {
val fruit = List("Banana", "Pineapple", "Apple");
assert(fruit.head == "Banana")
assertThrows[IndexOutOfBoundsException] {
fruit(5)
}
}
在这个测试中,我们首先确认列表的第一个元素是 "Banana"
,然后访问一个非法索引,验证是否抛出 IndexOutOfBoundsException
。
5. 使用不同测试风格
ScalaTest 支持多种测试风格,前面我们使用了 AnyFunSuite
,下面我们来看其他几种常见的风格。
5.1. 使用 AnyFlatSpec
AnyFlatSpec
旨在支持 BDD 风格的开发。它采用扁平结构,鼓励我们编写具有描述性名称的规范式测试。
✅ ScalaTest 官方推荐将 AnyFlatSpec
作为默认的测试风格。我们用它重写之前的例子:
class ListFlatSpec extends AnyFlatSpec {
"An empty List" should "have size 0" in {
assert(List.empty.size == 0)
}
it should "throw an IndexOutOfBoundsException when trying to access any element" in {
val emptyList = List();
assertThrows[IndexOutOfBoundsException] {
emptyList(1)
}
}
}
测试现在读起来更像是一个规范,即 “X should Y”。使用 it
是对上一个描述的引用。
运行测试后输出如下:
[info] ListFlatSpec:
[info] An empty List
[info] - should have size 0
[info] - should throw an IndexOutOfBoundsException when trying to access any element
5.2. 使用 AnyFunSpec
我们可以更进一步使用 AnyFunSpec
实现更深层次的嵌套结构:
class ListFunSpec extends FunSpec {
describe("A List") {
describe("when empty") {
it("should have size 0") {
assert(List.empty.size == 0)
}
it("should throw an IndexOutOfBoundsException when to access an element") {
val emptyList = List();
assertThrows[IndexOutOfBoundsException] {
emptyList(1)
}
}
}
}
}
这种语法对熟悉 RSpec 或 JavaScript 测试框架的开发者来说会很熟悉。
运行后输出:
[info] ListFunSpec:
[info] A List
[info] when empty
[info] - should have size 0
[info] - should throw an IndexOutOfBoundsException when to access an element
6. 测试前后处理
很多时候我们需要在测试前后执行一些通用逻辑,比如初始化资源或清理状态。
✅ 在 ScalaTest 中,我们可以混入 BeforeAndAfter
trait,并通过 before
和 after
代码块注册前置和后置逻辑:
class StringFlatSpecWithBeforeAndAfter extends AnyFlatSpec with BeforeAndAfter {
val builder = new StringBuilder;
before {
builder.append("Baeldung ")
}
after {
builder.clear()
}
// ...
在每个测试前,我们会向 builder
添加字符串;测试结束后清空它。
测试代码中可以直接使用 builder
:
"Baeldung" should "be interesting" in {
assert(builder.toString === "Baeldung ")
builder.append("is very interesting!")
assert(builder.toString === "Baeldung is very interesting!")
}
it should "have great tutorials" in {
assert(builder.toString === "Baeldung ")
builder.append("has great tutorials!")
assert(builder.toString === "Baeldung has great tutorials!")
}
⚠️ 注意:before
和 after
代码块只能通过副作用与测试通信,例如修改共享变量。
7. 使用 Matchers
前面我们使用的是标准断言,但 ScalaTest 提供了强大的 DSL —— Matchers
,使用 should
关键字来表达断言。
✅ 只需混入 Matchers
trait 即可使用:
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class ExampleFlatSpecWithMatchers extends AnyFlatSpec with Matchers {
...
7.1. 基础匹配器
我们来看一些常用的匹配器:
"A matcher" should "let us check equality" in {
val number = 25
number should equal (25)
number shouldEqual 25
}
it should "also let us check equality using be" in {
val number = 25
number should be (25)
number shouldBe 25
}
equal
和shouldEqual
用于检查相等性。be
和shouldBe
是另一种方式,不支持自定义相等性检查。- 如果需要自定义比较逻辑,可以使用
equal
并传入Equality[T]
。
例如:
it should "also let us customize equality" in {
" baeldung " should equal("baeldung")(after being trimmed)
}
还可以检查字符串长度或集合大小:
it should "let us check the length of strings" in {
val result = "baeldung"
result should have length 8
}
it should "let us check the size of collections" in {
val days = List("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
days should have size 7
}
7.2. 字符串匹配器
字符串匹配非常常见:
it should "let us check different parts of a string" in {
val headline = "Baeldung is really cool!"
headline should startWith("Baeldung")
headline should endWith("cool!")
headline should include("really")
}
验证邮箱格式:
it should "let us check that an email is valid" in {
"[email protected]" should fullyMatch regex """[^@]+@[^\.]+\..+"""
}
7.3. 其他常用匹配器
it should "let us check a list is empty" in {
List.empty shouldBe empty
}
it should "let us check a list is NOT empty" in {
List(1, 2, 3) should not be empty
}
it should "let us check a map contains a given key and value" in {
Map('x' -> 10, 'y' -> 20, 'z' -> 30) should contain('y' -> 20)
}
it should "let us check the type of an object" in {
List(1, 2, 3) shouldBe a[List[_]]
List(1, 2, 3) should not be a[Map[_, _]]
}
✅ 使用 Matchers
可以让测试代码更自然、更具可读性。
8. 测试标记
有时我们需要临时跳过某个测试,比如排查偶发失败时。
✅ ScalaTest 提供了 ignore
标记:
ignore should "let us check a list is empty" in {
List.empty shouldBe empty
}
运行测试时会看到:
[info] - should let us check a list is empty !!! IGNORED !!!
我们也可以自定义标记:
object BaeldungJavaTag extends Tag("com.baeldung.scala.scalatest.BaeldungJavaTag")
class TaggedFlatSpec extends AnyFlatSpec with Matchers {
"Baeldung" should "be interesting" taggedAs (BaeldungJavaTag) in {
"Baeldung has articles about Java" should include("Java")
}
}
运行特定标记的测试:
sbt "testOnly -- -n com.baeldung.scala.scalatest.BaeldungJavaTag"
9. 模拟对象(Mocking)
ScalaTest 支持任何 Java 模拟框架,也可以使用 ScalaMock。
✅ 首先添加依赖:
libraryDependencies += "org.scalamock" %% "scalamock" % "5.2.0" % Test
然后混入 MockFactory
:
class ScalaMockFlatSpec extends AnyFlatSpec with MockFactory with Matchers {
示例:
"A mocked Foo" should "return a mocked bar value" in {
val mockFoo = mock[Foo]
(mockFoo.bar _).expects().returning(6)
mockFoo.bar should be(6)
}
class Foo {
def bar = 100
}
10. 总结
本教程中,我们初步了解了 ScalaTest 测试框架的核心特性:
- ✅ 如何编写第一个测试
- ✅ 多种测试风格的使用
- ✅ Matchers 的强大表达能力
- ✅ 测试标记与模拟对象的使用
更多详细内容可参考 ScalaTest 官方文档。
源码可在 GitHub 获取。