Scala 的单测框架和示例(JUnit4 & ScalaTest)


随着下一代大数据计算框架 Spark 的流行,Scala 也受到了越来越多的关注。在 Scala 开发中,免不了需要编写单元测试,这样可以提升开发效率,方便地进行回归测试。在实际业务开发中,业务需求变更会引入大量的逻辑,通过单元测试可以很好地保障现有代码的逻辑依然符合预期,前提是单元测试的 case 覆盖较为全面。

本文主要介绍了两种单元测试的框架,一种是传统的 Java 的 JUnit4 框架,另一种是 Scala 比较常用的 ScalaTest 框架。针对两个框架,各自简单介绍,并提供示例代码,笔者使用中碰到的问题解决,希望可以帮到有需要的朋友。

JUnit4

JUnit 是 Java 中最常用的单测框架,目前已经有 JUnit5,本文暂时只讨论 JUnit4,笔者还细致了解过 JUnit5。

在 Scala 中也可以使用与 Java 一样的 Annotation 的方式来标记测试方法。

import org.junit.Assert._
import org.junit._

class UsingJUnit {

  @Before
  def before(): Unit = {
    println("before test")
  }

  @After
  def after(): Unit = {
    println("after test")
  }

  @Test
  def testList(): Unit = {
    println("testList")
    val list = List("a", "b")

    assertEquals(List("a", "b"), list)
    assertNotEquals(List("b", "a"), list)
  }
}

断言的使用也基本和 Java 保持一致,Before 和 After 是在每个测试用例的前后执行。

JUnit4 还提供了 BeforeClass 和 AfterClass 在整个测试的前后执行一次,要求方法必须是 static 的。在 Java 中通过 static 关键字来定义,在 Scala 中需要实用伴生对象,定义一个 object 来实现 static 方法。

object UsingJUnit {
  @BeforeClass
  def beforeClass(): Unit = {
    println("before class")
  }

  @AfterClass
  def afterClass(): Unit = {
    println("after class")
  }
}

除了 static 的方法外,基本与 Java 保持了一致。

JUnit 检查异常

我们在编写代码的时候,会预期抛出一些异常,对这些异常的检查,也是单测中需要做的事情。下面举例,说明异常检测的方法,一种只检查抛出的异常类,另外一种是检查异常类的类型和 Message 信息。

Scala 在使用 JUnit @Rule 的时候有些问题,一定需要是 public 才可以,下面是一个变通的实现方式。

import org.junit.Assert._
import org.junit._

class JunitCheckException {

  val _thrown = rules.ExpectedException.none

  @Rule
  def thrown = _thrown

  @Test(expected = classOf[IndexOutOfBoundsException])
  def testStringIndexOutOfBounds(): Unit = {
    val s = "test string"
    s.charAt(-1)
  }

  @Test
  def testStringIndexOutOfBoundsExceptionMessage(): Unit = {
    val s = "test string"
    thrown.expect(classOf[IndexOutOfBoundsException])
    thrown.expectMessage("String index out of range: -1")
    s.charAt(-1)
  }
}

JUnit 完整示例

ScalaTest

ScalaTest 是 Scala 原生的单元测试框架,本身与 JUnit 没有太多相似之处,功能强大,目前笔者只了解到了少量功能,其他功能仍需继续探索。

ScalaTest 加入了很多的 DSL,使得测试的描述融合在代码中。先看一个示例(从 Scala 实用指南中的示例少许更改而来)

import org.scalatest._

class UsingScalaTest1 extends FlatSpec with Matchers {
  trait EmptyArrayList {
    val list = new java.util.ArrayList[String]
  }

  "a list" should "be empty on create" in new EmptyArrayList {
    list.size shouldBe 0
  }

  it should "increase in size upon add" in new EmptyArrayList {
    list.add("Milk")
    list add "Sugar"

    list.size should be(2)
  }
}

代码中对测试 case 的描述,会在执行后格式化输出到终端,这点非常方便,可以很方便地查看。

UsingScalaTest1:
a list
- should be empty on create
- should increase in size upon add

maven 配置 plugin

在与 maven 继承的过程中,需要配置相应的插件才可以。参考资料 5。

...
<build>
    <plugins>
        ...
        <!-- disable surefire -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.7</version>
            <configuration>
                <skipTests>true</skipTests>
            </configuration>
        </plugin>

        <!-- enable scalatest -->
        <plugin>
            <groupId>org.scalatest</groupId>
            <artifactId>scalatest-maven-plugin</artifactId>
            <version>1.0</version>
            <configuration>
                <reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
                <junitxml>.</junitxml>
                <filereports>WDF TestSuite.txt</filereports>
            </configuration>
            <executions>
                <execution>
                    <id>test</id>
                    <goals>
                        <goal>test</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
...

需要说明的是 disable surefire 这步,其实可以不用,这样可以将 JUnit 和 ScalaTest 的测试用例都跑一下,并不冲突。

ScalaTest 检测异常

ScalaTest 也同样提供了对异常的检测,而且易用性更好,更加像英语表达。

// 验证抛出的异常
// check the exception thrown by method
class ScalaTestCheckException extends FlatSpec with Matchers {
  "string" should "throw IndexOutOfBoundsException when index is illegal" in {
    val s = "test string"
    an [IndexOutOfBoundsException] should be thrownBy s.charAt(-1)

    val thrown = the [IndexOutOfBoundsException] thrownBy s.charAt(-1)
    thrown.getMessage should equal ("String index out of range: -1")
  }
}

trait 的用法

ScalaTest 提供了大量的 trait,可以使用各类的功能,本文只介绍其中三种:BeforeAndAfter、BeforeAndAfterAll 和 GivenWhenThen。

BeforeAndAfter 可以实现与 JUnit 的 Before 和 After 相同的功能,在每个测试用例的前后执行。类似的,BeforeAndAfterAll 就是与 JUnit 的 BeforeClass 和 AfterClass 一致。

GivenWhenThen 是 ScalaTest 特有的,是用来对 case 内部的细节进行描述的,最终会被格式化输出到终端中。

下面通过几个代码示例来了解其具体用法。

BeforeAndAfter

class UsingBeforeAndAfter extends FlatSpec with BeforeAndAfter with Matchers {

  var list: java.util.List[String] = _

  before {
    println("before")
    list = new java.util.ArrayList[String]
  }

  after {
    println("after")
    if (list != null) {
      list = null
    }
  }

  "a list" should "be empty on create" in {
    list.size shouldBe 0
  }

  it should "increase in size upon add" in {
    list.add("Milk")
    list add "Sugar"

    list.size should be(2)
  }
}

测试的输出如下

before
after
before
after
UsingBeforeAndAfter:
a list
- should be empty on create
- should increase in size upon add

before 和 after 打印了两次,因为每个测试 case 的前后都需要执行一次。

BeforeAndAfterAll

class UsingBeforeAndAfterAll extends FlatSpec with BeforeAndAfterAll with Matchers {

  var list: java.util.List[String] = _

  override def beforeAll(): Unit = {
    println("before all...")
    list = new java.util.ArrayList[String]
    super.beforeAll()
  }

  override def afterAll(): Unit = {
    println("after all...")
    if (list != null) {
      list = null
    }
    super.afterAll()
  }

  "a list" should "be empty on create" in {
    list.size shouldBe 0
  }

  it should "increase in size upon add" in {
    list.add("Milk")
    list add "Sugar"

    list.size should be(2)
  }
}

测试的输出如下

before all...
after all...
UsingBeforeAndAfterAll:
a list
- should be empty on create
- should increase in size upon add

before all 和 after all 只打印了一次,因为这个是 test suite 中只执行一次。

GivenWhenThen

class UsingGivenWhenThen extends FlatSpec with GivenWhenThen with Matchers {
  trait EmptyArrayList {
    val list = new java.util.ArrayList[String]
  }

  "a list" should "be empty on create" in new EmptyArrayList {
    Given("a empty list")
    Then("list size should be 0")
    list.size shouldBe 0
  }

  it should "increase in size upon add" in new EmptyArrayList {
    Given("a empty list")

    When("add 2 elements")
    list.add("Milk")
    list add "Sugar"

    Then("list size should be 2")
    list.size should be(2)
  }
}

输出如下:

UsingGivenWhenThen:
a list
- should be empty on create
  + Given a empty list
  + Then list size should be 0
- should increase in size upon add
  + Given a empty list
  + When add 2 elements
  + Then list size should be 2

我们可以看到通过使用 Given、When、Then 三个方法,可以对最终的输出有影响,描述性更强,三段式的描述可以满足大多数场景。

ScalaTest 完整示例

总结

本文的示例相对比较简单,对于当前笔者的需求来说是足够的,以实用和快速上手为主,更为细致的功能,还需要继续学习和探索。

参考资料

  1. JUnit4
  2. ScalaTest
  3. Scala and Apache Spark in Tandem as a Next-Generation ETL Framework
  4. Scala 实用指南 - 第16章 单元测试
  5. Using the ScalaTest Maven plugin
  6. (StackOverflow) Using JUnit @Rule with ScalaTest (e.g. TemporaryFolder)

如果觉得文章对您有帮助,用微信请作者喝杯咖啡吧!这样他会更有动力,分享更多更好的知识!

wechat赞赏