A Hands-On Introduction to ScalaTest

Learn how to write comprehensive Scala tests tailored for your specific needs with the help of ScalaTest and its wide selection of testing styles.

Brought to you by

Semaphore

Introduction

You have to eat your vegetables to grow and become stronger, ‘cause one day you’ll have to program in Scala - moms always say. We don’t pay too much attention to what they're saying until we have to face the challenge. When you have no background in Java, programming in Scala can be easier. However, in the end we usually realize that what everybody says holds true: the Scala learning curve is kind of steep, not because of the syntax, but because of the paradigm it uses — the functional one.

After a lot of reading, asking questions in forums, and programming with Scala for some years, you get used to the language. But then you realize that there are a lot of things, such as code testing, that you usually managed to take care of without much thought, but that you don't really know how to handle in a functional way. In this tutorial, we’ll learn how to effectively deal with testing Scala applications.

Prerequisites

Make sure you have the following before reading further:

  • sbt 0.13.8 or higher installed.
  • Some notions about Scala.
  • A greedy desire to learn!

Our Domain Code

In order to shed some light on the main concepts, let’s suppose we have some Scala classes we've just implemented and want to test. For example, let's take a look at classes from a simple snippet that models a hotel, and the action of checking in:

case class Guest(name: String)

case class Room(number: Int, guest: Option[Guest] = None){ room =>

  def isAvailable(): Boolean = ???

  def checkin(guest: Guest): Room = ???

  def checkout(): Room = ???

}

/*
 * We will automatically create 10 rooms
 * if these are not specified.
 */
case class Hotel(
  rooms: List[Room] = (1 to 10).map(n => Room(number=n)).toList){

  def checkin(personName: String): Hotel = ???

}

As you can see, we used the ??? notation to leave the methods unimplemented. If we want to use the TDD approach, we first need to define the expected behavior of these components by creating a bunch of tests.

Before doing this, let’s organize and put them into a proper SBT project. We can start by adding an SBT project definition (build.sbt). In this file, we will include some info about the name of our project and the version of Scala we're currently using:

name := "hotel-management"

version := "0.1-SNAPSHOT"

scalaVersion := "2.10.4"

Next, we will create the class files, preferably one per class. The scaffolding should look like this:

hotel-management/
  src/
    main/
      resources/ (all resource data should be allocated here)
      scala/
        org.me.hotel/
          Guest.scala
          Hotel.scala
          Room.scala
    test/
      resources/ (all resources used in the test should be allocated here)
      scala/
        org.me.hotel/ (all test classes should go in here)
  build.sbt

Testing Frameworks

For testing these brand new classes, we first need to choose a proper testing framework. There are two outstanding Scala testing frameworks: ScalaTest and ScalaCheck. Both are easy to use, but there are a few differences depending on the test approach you want to use.

For example, ScalaCheck is a great choice if we want to define some property-based tests. Without giving too many details, for example we can define the way to test a Room’s check-in status:

object RoomSpec extends Properties(Room){
  property(is available when theres no guest) =
    forAll{ (room: Room) =>
      room.isAvailable() == room.guest.isEmpty
    }
}

This specification for Rooms indicates that the property that rules if the room is available has to check for all possible rooms. This property is directly related to the existence of a guest that occupies the room. How can ScalaCheck make it possible to test this property for all possible rooms? We can do this by automatically generating a bunch of them.

On the other hand, what can ScalaTest offer us? In addition to the ease of integrating with ScalaCheck, what's also great about ScalaTest are its capabilities and ease of use. It’s a versatile testing framework that allows testing both Java and Scala code. What’s also remarkable is its integration with powerful tools such as JUnit, TestNG, Ant, Maven, sbt, ScalaCheck, JMock, EasyMock, Mockito, ScalaMock, Selenium (browser testing automation), etc.

In the next few paragraphs, we'll see how to use ScalaTest specs for testing the classes we added to our recently created SBT project.

Adding ScalaTest Dependencies to Our SBT Project

We will start by appending a ScalaTest dependency at the end of our build.sbt file:

libraryDependencies += "org.scalatest" %% "scalatest" % "2.2.4" % "test"

Make sure to check the Scala version you're using. Remember that %% represents the addition of the current version of Scala to the artifact's name. In this case, it equals to:

libraryDependencies += "org.scalatest" % "scalatest_2.10" % "2.2.4" % "test"

Choosing a Testing Style

ScalaTest offers several testing styles: property-based tests (PropSpec), descriptive-language-based testing (WordSpec), simple styles (FunSpec), etc.

All of them are very easy to use, and can provide a higher level abstraction layer. Our suggestion consists of choosing one style for unit testing and another one for acceptance. This is due to the need to maintain uniformity across the same type of tests. When a developer has to change from creating unit tests to creating acceptance ones, this difference helps to change the thought process about the type of task they're doing.

For this example, we will choose a pretty legible testing style such as FlatSpec.

Base Classes

Base classes are fully recommended because they allow us to gather all the traits we will use for all of our test classes, just in order to reuse all common functionality without too much boilerplate.

For example, we can define a class called UnitTest in the org.me.hotel package.

Let's create a file called UnitTest.scala in the hotel-management/src/test/org/me/hotel directory with the following content:

package org.me.hotel

import org.scalatest.{FlatSpec,Matchers}

abstract class UnitTest(component: String) extends FlatSpec
  with Matchers{

  behavior of component

}

By extending FlatSpec, we're choosing the testing style, and by mixing it with Matchers, we are including some additional functionality that we'll explain below.

The behavior of component statement defines the name of the component that is being tested and the way in which the class that extends our base class will be printed out. For example, if we define a test class like this:

class MyClassUnitTest extends UnitTest("MyClass")

when launching the test class, it will print out the behavior of "MyClass".

Using Matchers

As you could previously see, our base class is mixing in with the Matchers trait. This provides an easy way that's very close to a human language to express assertions and conditions that need to be checked in our tests. For example, we can express requirements like these:

(2+2) should equal (4)
(2+2) shouldEqual 5
(2+2) should === (4)
(2+2) should be (4)
(2+2) shouldBe 5

If any of them fails (for example, the last one), an exception like the following will be thrown: 4 did not equal 5.

If we are expecting some kind of exception, even though this approach is not very functional, we can assert it with:

an [IndexOutOfBoundsException] should be thrownBy "my string".charAt(-1)

There are many other ways to express assertions like greater and less than, regular expressions in Strings, type checking, etc. For example:

// greater and less than

1 should be < 3
1 should be > 0
1 should be >= 0
1 should be <= 2

// reg. exp.

"Hello friends" should startWith ("Hello")
"You rock my world" should endWith ("world")
"In my dreams" should include ("my")

// type checking

1 shouldBe a [Int]
true shouldBe a [Boolean]
Guest("Alfred") shouldBe a [Guest]

Defining Test Classes

If we apply all we have learned to our domain classes, we can define the Room specification.

First, we need to create a file called RoomTest.scala in the hotel-management/src/test/scala/org/me/hotel/ directory, and add the following code:

package org.me.hotel

class RoomTest extends UnitTest("Room") {

  it should "provide info about its occupation" in {
    Room(1).isAvailable() shouldEqual true
    Room(1,None).isAvailable() shouldEqual true
    Room(1,Some(Guest("Bruce"))).isAvailable() shouldEqual false
  }

  it should "allow registering a new guest if room is available" in {
    val occupiedRoom = Room(1).checkin(Guest("James"))
    occupiedRoom.isAvailable shouldEqual false
    occupiedRoom.guest shouldEqual(Option(Guest("James")))
  }

  it should "deny registering a new guest if room is already occupied" in {
    an [IllegalArgumentException] should be thrownBy {
      Room(1,Some(Guest("Barbara"))).checkin(Guest("Bruce"))
    }
  }

  it should "deny checking out if room is already available" in {
    an [IllegalArgumentException] should be thrownBy {
      Room(1).checkout()
    }
  }

  it should "allow checking out if room is occupied by someone" in {
    val room = Room(1,Some(Guest("Carmine")))
    val availableRoom = room.checkout()
    availableRoom.isAvailable shouldEqual true
  }

}

As you can see, test cases are defined with

it should "your-test-case-description" in { /* your test code */ }

For example, in the first test case, we're checking if the isAvailable() method result is directly related to the existence of a guest that occupies the room. We can also highlight those test cases where we want to catch some exception, like the case where we are trying to register a guest in an already occupied room.

Let's Test It!

Once you have implemented your test classes, you can run your tests by using the SBT task called test. If we launch this right now, we'll have some problems with unimplemented methods (remember that we used ??? for leaving them unimplemented). Tests will fail and the result output will be as follows:

> test
[info] RoomTest:
[info] Room
[info] - should provide info about its occupation *** FAILED ***
[info]   scala.NotImplementedError: an implementation is missing
[info]   at scala.Predef$.$qmark$qmark$qmark(Predef.scala:252)
[info]   at org.me.hotel.Room.isAvailable(Room.scala:6)
[info]   at org.me.hotel.RoomTest$$anonfun$1.apply$mcV$sp(RoomTest.scala:6)
[info]   at org.me.hotel.RoomTest$$anonfun$1.apply(RoomTest.scala:5)

...

[info] Run completed in 699 milliseconds.
[info] Total number of tests run: 15
[info] Suites: completed 6, aborted 0
[info] Tests: succeeded 3, failed 12, canceled 0, ignored 0, pending 0
[info] *** 12 TESTS FAILED ***
[error] Failed tests:
[error]   org.me.hotel.RoomTest
[error]   org.me.hotel.HotelTest
[error] (test:test) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 6 s, completed 12-ago-2015 16:08:47

Now, let's implement abstract methods in order to make these tests run. The Room.scala file should look as follows:

package org.me.hotel

case class Room(number: Int, guest: Option[Guest] = None){ room =>

  def isAvailable(): Boolean =
    guest.isEmpty

  def checkin(guest: Guest): Room = {
    require(room.guest.isEmpty, "Room is occupied")
    Room(number,Some(guest))
  }

  def checkout(): Room = {
    require(guest.isDefined,"Room is already available")
    Room(number,None)
  }

}

After having implemented all abstract methods, if we execute test again, this will launch a test discovery in your project and, once finished, it will launch all discovered test classes, showing something similar to:

> test
[info] GuestTest:
[info] RoomTest:
[info] Guest
[info] Room
[info] - should have its name defined
[info] - should provide info about its occupation
[info] - should allow registering a new guest if room is available
[info] - should deny registering a new guest if room is already occupied
[info] - should deny checking out if room is already available
[info] RoomTest:
[info] Room
[info] - should provide info about its occupation
[info] - should allow registering a new guest if room is available
[info] - should deny registering a new guest if room is already occupied
[info] - should deny checking out if room is already available
[info] - should allow checking out if room is occupied by someone
[info] - should allow checking out if room is occupied by someone
[info] GuestTest:
[info] Guest
[info] - should have its name defined
[info] HotelTest:
[info] Hotel
[info] - should forbid creating a Hotel with no rooms
[info] - should forbid checking in if there are no available rooms
[info] - should allow checking in
[info] Run completed in 859 milliseconds.
[info] Total number of tests run: 15
[info] Suites: completed 6, aborted 0
[info] Tests: succeeded 15, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 1 s, completed 04-ago-2015 18:23:13

Ignoring Test Cases

Another useful feature worth mentioning allows us to avoid testing all test cases. If we want to ignore some of them, we don't have to comment out our test code, or even the test case code; we can ignore it by replacing it with ignore. For example:

//...

class RoomTest extends UnitTest("Room") {

  ignore should "provide info about its occupation" in {
    //...
  }

  it should "allow registering a new guest if room is available" in {
    //...
  }

  //...

If we ignore some test cases, the result will look like this:

> test
[info] GuestTest:
[info] RoomTest:
[info] Guest
[info] Room
[info] - should have its name defined !!! IGNORED !!!
[info] - should provide info about its occupation
[info] - should allow registering a new guest if room is available !!! IGNORED !!!
[info] - should deny registering a new guest if room is already occupied
[info] - should deny checking out if room is already available
[info] RoomTest:
[info] Room
[info] - should provide info about its occupation
[info] - should allow registering a new guest if room is available !!! IGNORED !!!
[info] - should deny registering a new guest if room is already occupied
[info] - should deny checking out if room is already available
[info] - should allow checking out if room is occupied by someone
[info] - should allow checking out if room is occupied by someone
[info] GuestTest:
[info] Guest
[info] - should have its name defined
[info] HotelTest:
[info] Hotel
[info] - should forbid creating a Hotel with no rooms
[info] - should forbid checking in if there are no available rooms
[info] - should allow checking in
[info] Run completed in 859 milliseconds.
[info] Total number of tests run: 15
[info] Suites: completed 6, aborted 0
[info] Tests: succeeded 12, failed 0, canceled 0, ignored 3, pending 0
[info] All tests passed.
[success] Total time: 1 s, completed 04-ago-2015 18:23:13

Testing a Single Spec

If we have several test classes and we want to test only one of them, we can also use the SBT task test-only (or testOnly depending on the SBT version).

> test-only org.me.hotel.GuestTest
[info] GuestTest:
[info] Guest
[info] - should have its name defined
[info] Run completed in 2 seconds, 262 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.
[success] Total time: 29 s, completed 04-ago-2015 18:21:52

Other Useful Concepts

Grouping Test Classes

As previously demonstrated, we can launch all discovered tests in the current project by using test discovery, but we might just want to launch a bunch of them. For grouping tests, we can use a special class called Suites. For example:

package org.me.hotel

class MySuites extends Suites(
  new GuestTest,
  new RoomTest)

This way, when executing the SBT task test-only org.me.hotel.MySuite, only GuestTest and RoomTest will be launched, instead of all test classes.

Have in mind that, if you run the test task after having defined at least some Suites, there will be repeated test executions — one for the Suites, and one for the test itself.

Parallel Tests

What about parallelism? If our tests are properly designed, and can run independently, we can execute all of them in parallel by adding an SBT definition file:

testForkedParallel in Test := true

If you only want to run tests sequentially, but in a different JVM, you can achieve this by adding:

fork in Test := true

Conclusion

As you can see, ScalaTest is quite a powerful testing framework, with which you can start working after learning only a few concepts. The similarity with the user's language makes it really easy to make use of a wide range of its features.

All used code examples can also be found here.

3f6bbcdd6cc14e747ca1c89827a9486e
Javier Santos

@JPaniego, passionate about functional programming, DSLs and Big data projects; started playing with Scala in 2011. He also blogs at Scalera with @dvnavarro.

on this tutorial so far.
User deleted author {{comment.createdAt}}

Edited on {{comment.updatedAt}}

Cancel

Sign In You must be logged in to comment.