Programming in Scala [Chapter 14] — Assertions and Unit Testing

Tom van Eijk
7 min readNov 13, 2023

--

Photo by Dmitry Ratushny on Unsplash

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:

  1. Assertions
  2. Unit testing in Scala
  3. Informative failure reports
  4. Using JUnit and TestNG
  5. Tests as specifications
  6. Property-based testing
  7. 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.

--

--

Tom van Eijk

Data Enthusiast who loves to write data engineering blogs for learning purposes