Programming in Scala [Chapter 14] — Assertions and Unit Testing
Introduction
This is part of my personal reading summary of the book — Programming in Scala. Take into account, we use Scala 2 since it is in industry and has vastly more resources available than Scala 3.
There are two crucial methods for ensuring that the software you develop behaves as intended: using assertions and conducting unit tests. This chapter will explore various Scala options for creating and executing these checks through the following sections:
- Assertions
- Unit testing in Scala
- Informative failure reports
- Using JUnit and TestNG
- Tests as specifications
- Property-based testing
- Organizing and running tests
1. Assertions
In Scala, assertions are written using the assert
method, which throws an AssertionError
if the specified condition is false. There's a two-argument version allowing for an explanation upon failure. For instance, in the above
method of the Element
class below, an assertion is used to ensure equal widths after a transformation. Alternatively, the ensuring
method in Predef
facilitates concise checks on a result type by using a predicate function. This example demonstrates asserting that the width condition holds for the widen
method's result.
def above(that: Element): Element = {
val this1 = this widen that.width
val that1 = that widen this.width
assert(this1.width == that1.width)
elem(this1.contents ++ that1.contents)
}
private 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)
2. Unit testing in Scala
In Scala, there are various options for unit testing, including established Java tools like JUnit and TestNG, as well as Scala-specific tools like ScalaTest, specs, and ScalaCheck. ScalaTest provides flexibility in writing tests, with the simplest approach involving the creation of classes that extend org.scalatest.Suite
and defining test methods within them. For instance:
import org.scalatest.Suite
import Element.elem
class ElementSuite extends Suite {
def testUniformElement() {
val ele = elem('x', 2, 3)
assert(ele.width == 2)
}
}
While ScalaTest includes a Runner application, you can also directly run a Suite from the Scala interpreter. The execute
method in the Suite
trait uses reflection to discover and invoke test methods. Example:
scala> (new ElementSuite).execute()
Test Starting - ElementSuite.testUniformElement
Test Succeeded - ElementSuite.testUniformElement
ScalaTest supports different testing styles, allowing customization by overriding the execute
method in Suite subtypes. For example, the trait FunSuite
provides a different style, allowing you to define tests as function values rather than methods:
import org.scalatest.FunSuite
import Element.elem
class ElementSuite extends FunSuite {
test("elem result should have passed width") {
val ele = elem('x', 2, 3)
assert(ele.width == 2)
}
}
In FunSuite
, the "Fun" stands for function, and the test
method is invoked by the primary constructor, allowing you to define tests as function values. This approach provides flexibility in naming tests and avoids the necessity of starting all test names with "test."
3. Informative failure reports
Assertions play a key role in validating software behavior. The triple-equals operator in ScalaTest offers a concise way to compare values in assertions:
assert(ele.width === 2)
If the assertion fails, the error message specifies the mismatched values. To emphasize the distinction between expected and actual results, ScalaTest provides the expect
method:
expect(2) {
ele.width
}
This ensures clarity in failure reports. Additionally, for checking expected exceptions, ScalaTest’s intercept
method proves useful:
intercept(classOf[IllegalArgumentException]) {
elem('x', -2, 3)
}
It captures and allows further inspection of the thrown exception. If the expected exception is not thrown, or a different one occurs, it triggers an AssertionError with a descriptive error message.
4. Using JUnit and TestNG
The primary unit testing framework on the Java platform is JUnit, and Scala provides seamless integration with it. Below is an example using JUnit:
import junit.framework.TestCase
import junit.framework.Assert.assertEquals
import junit.framework.Assert.fail
import Element.elem
class ElementTestCase extends TestCase {
def testUniformElement() {
val ele = elem('x', 2, 3)
assertEquals(2, ele.width)
assertEquals(3, ele.height)
try {
elem('x', -2, 3)
fail()
} catch {
case e: IllegalArgumentException => // expected
}
}
}
If you prefer ScalaTest’s assertion syntax, you can use JUnit3Suite
:
import org.scalatest.junit.JUnit3Suite
import Element.elem
class ElementSuite extends JUnit3Suite {
def testUniformElement() {
val ele = elem('x', 2, 3)
assert(ele.width === 2)
expect(3) { ele.height }
intercept(classOf[IllegalArgumentException]) {
elem('x', -2, 3)
}
}
}
ScalaTest also integrates with other frameworks, such as TestNG. Here’s an example using TestNG:
import org.testng.annotations.Test
import org.testng.Assert.assertEquals
import Element.elem
class ElementTests {
@Test def verifyUniformElement() {
val ele = elem('x', 2, 3)
assertEquals(ele.width, 2)
assertEquals(ele.height, 3)
}
@Test {
val expectedExceptions = Array(classOf[IllegalArgumentException])
}
def elemShouldThrowIAE() { elem('x', -2, 3) }
}
For TestNG with ScalaTest’s syntax:
import org.scalatest.testng.TestNGSuite
import org.testng.annotations.Test
import Element.elem
class ElementSuite extends TestNGSuite {
@Test def verifyUniformElement() {
val ele = elem('x', 2, 3)
assert(ele.width === 2)
expect(3) { ele.height }
intercept(classOf[IllegalArgumentException]) {
elem('x', -2, 3)
}
}
}
5. Tests as specifications
In behavior-driven development (BDD), ScalaTest’s Spec trait is employed to create human-readable specifications and corresponding tests, emphasizing the expected behavior of the code. Describers and specifiers within a Spec define the subject and its specific behaviors, respectively. Executing a Spec generates output resembling a specification. Here’s an example using ScalaTest:
import org.scalatest.Spec
class ElementSpec extends Spec {
"A UniformElement" -- {
"should have a width equal to the passed value" - {
val ele = elem('x', 2, 3)
assert(ele.width === 2)
}
"should have a height equal to the passed value" - {
val ele = elem('x', 2, 3)
assert(ele.height === 3)
}
"should throw an IAE if passed a negative width" - {
intercept(classOf[IllegalArgumentException]) {
elem('x', -2, 3)
}
}
}
}
Alternatively, the specs testing framework offers a BDD-style syntax with expressive matchers for natural language assertions. Here’s an example using specs:
import org.specs._
object ElementSpecification extends Specification {
"A UniformElement" should {
"have a width equal to the passed value" in {
val ele = elem('x', 2, 3)
ele.width must be_==(2)
}
"have a height equal to the passed value" in {
val ele = elem('x', 2, 3)
ele.height must be_==(3)
}
"throw an IAE if passed a negative width" in {
elem('x', -2, 3) must throwA(new IllegalArgumentException)
}
}
}
Specs aim to create assertions in a readable format with descriptive failure messages. Matchers like “must be_==” and “must throwA” demonstrate specs’ syntax.
6. Property-based testing
ScalaCheck is a valuable testing tool for Scala, allowing you to specify properties that your code must adhere to. It generates test data and checks whether these properties hold true. The following example demonstrates the use of ScalaCheck within a ScalaTest suite:
import org.scalatest.prop.FunSuite
import org.scalacheck.Prop._
import Element.elem
class ElementSuite extends FunSuite {
test("elem result should have passed width", (w: Int) =>
w > 0 ==> (elem('x', w, 3).width == w)
)
test("elem result should have passed height", (h: Int) =>
h > 0 ==> (elem('x', 2, h).height == h)
)
}
In this code snippet, properties are expressed as functions taking generated test data. The ==>
operator implies that the right-hand expression must hold true when the left-hand expression is true. If a property fails for any generated value, ScalaCheck raises an AssertionError
with detailed information.
For more concise checks, ScalaTest’s Checkers
trait can be mixed into your test class, allowing multiple property checks within a single test or a combination of property checks and assertions:
import org.scalatest.junit.JUnit3Suite
import org.scalatest.prop.Checkers
import org.scalacheck.Prop._
import Element.elem
class ElementSuite extends JUnit3Suite with Checkers {
def testUniformElement() {
check((w: Int) => w > 0 ==> (elem('x', w, 3).width == w))
check((h: Int) => h > 0 ==> (elem('x', 2, h).height == h))
}
}
This example demonstrates the same property checks as before, this time within a single test using ScalaTest’s Checkers
trait.
7. Organizing and running tests
In ScalaTest, you organize tests into suites, forming a tree structure. Suites can be nested manually by overriding the nestedSuites
method or automatically by providing package names to ScalaTest's Runner. The Runner executes the root suite, which triggers the execution of all nested suites.
To run tests using ScalaTest’s Runner, you can use the command line or an ant task. Specify the suites to run, either explicitly or by indicating name prefixes for automatic discovery. Additionally, you can define a runpath, reporters for result presentation, and load class files from specified directories and JAR files.
For example, to run the SuiteSuite from the ScalaTest distribution, use the following command:
$ scala -cp scalatest-0.9.4.jar org.scalatest.tools.Runner -p "scalatest-0.9.4-tests.jar" -s org.scalatest.SuiteSuite
Here, -cp
sets the classpath, org.scalatest.tools.Runner
is the Runner application, -p
specifies the runpath (JAR file), and -s
indicates the suite to execute. The default graphical reporter will display the test results.
Concluding Thoughts
This chapter summarizes key testing concepts. It covers assertions, unit tests using ScalaTest, JUnit, and TestNG integration. The importance of informative failure reports is highlighted. The chapter explores tests as specifications with ScalaTest’s Spec trait and introduces property-based testing with ScalaCheck. The organization and execution of tests in ScalaTest are discussed briefly, including manual and automatic suite nesting. A specific example of running a suite from ScalaTest’s Runner application is provided.
With this, we conclude the fourteenth chapter of this series. I hope it adds value to people who are interested in learning Scala.
Please don’t hesitate to contact me on LinkedIn with any comments or feedback.
Other chapters in this series can be found in this reading list.
Resources:
Odersky, M., Spoon, L., & Venners, B. (2008). Programming in scala. Artima Inc.