ScalaTest User Guide

Getting started

Selecting testing styles

Defining base classes

Writing your first test

Using assertions

Tagging your tests

Running your tests

Sharing fixtures

Sharing tests

Using matchers

Testing with mock objects

Property-based testing

Asynchronous testing

Using Scala-js

Using Inside

Using OptionValues

Using EitherValues

Using PartialFunctionValues

Using PrivateMethodTester

Using WrapWith

Philosophy and design

Migrating to 3.0

Table-driven property checks

To use table-driven property checks, you must mix in trait TableDrivenPropertyChecks (or import the members of its companion object). If you are also using ScalaCheck generator-driven property checks, you can mix in trait ScalaCheckPropertyChecks, which extends both TableDrivenPropertyChecks and ScalaCheckDrivenPropertyChecks.

Trait TableDrivenPropertyChecks contains one forAll method for each TableForN class, TableFor1 through TableFor22, which allow properties to be checked against the rows of a table. It also contains a wherever method that can be used to indicate a property need only hold whenever some condition is true.

For an example of trait TableDrivenPropertyChecks in action, imagine you want to test this Fraction class:

class Fraction(n: Int, d: Int) {

require(d != 0) require(d != Integer.MIN_VALUE) require(n != Integer.MIN_VALUE)
val numer = if (d < 0) -1 * n else n val denom = d.abs
override def toString = numer + " / " + denom }

TableDrivenPropertyChecks allows you to create tables with between 1 and 22 columns and any number of rows. You create a table by passing tuples to one of the factory methods of object Table. Each tuple must have the same arity (number of members). The first tuple you pass must all be strings, because it define names for the columns. Subsequent tuples define the data. After the initial tuple that contains string column names, all tuples must have the same type. For example, if the first tuple after the column names contains two Ints, all subsequent tuples must contain two Int (i.e., have type Tuple2[Int, Int]).

To test the behavior of Fraction, you could create a table of numerators and denominators to pass to the constructor of the Fraction class using one of the apply factory methods declared in Table, like this:

import org.scalatest.prop.TableDrivenPropertyChecks._

val fractions = Table( ("n", "d"), // First tuple defines column names ( 1, 2), // Subsequent tuples define the data ( -1, 2), ( 1, -2), ( -1, -2), ( 3, 1), ( -3, 1), ( -3, 0), ( 3, -1), ( 3, Integer.MIN_VALUE), (Integer.MIN_VALUE, 3), ( -3, -1) )

You could then check a property against each row of the table using a forAll method, like this:

import org.scalatest.matchers.should.Matchers._

forAll (fractions) { (n: Int, d: Int) =>
whenever (d != 0 && d != Integer.MIN_VALUE && n != Integer.MIN_VALUE) {
val f = new Fraction(n, d)
if (n < 0 && d < 0 || n > 0 && d > 0) f.numer should be > 0 else if (n != 0) f.numer should be < 0 else f.numer should be === 0
f.denom should be > 0 } }

Trait TableDrivenPropertyChecks provides 22 overloaded forAll methods that allow you to check properties using the data provided by a table. Each forAll method takes two parameter lists. The first parameter list is a table. The second parameter list is a function whose argument types and number matches that of the tuples in the table. For example, if the tuples in the table supplied to forAll each contain an Int, a String, and a List[Char], then the function supplied to forAll must take 3 parameters, an Int, a String, and a List[Char]. The forAll method will pass each row of data to the function, and generate a TableDrivenPropertyCheckFailedException if the function completes abruptly for any row of data with any exception that would normally cause a test to fail in ScalaTest other than DiscardedEvaluationException. An DiscardedEvaluationException, which is thrown by the whenever method (also defined in this trait) to indicate a condition required by the property function is not met by a row of passed data, will simply cause forAll to skip that row of data.

Testing stateful functions

One way to use a table with one column is to test subsequent return values of a stateful function. Imagine, for example, you had an object named FiboGen whose next method returned the next fibonacci number, where next means the next number in the series following the number previously returned by next. So the first time next was called, it would return 0. The next time it was called it would return 1. Then 1. Then 2. Then 3, and so on. FiboGen would need to maintain state, because it has to remember where it is in the series. In such a situation, you could create a TableFor1 (a table with one column, which you could alternatively think of as one row), in which each row represents the next value you expect.

val first14FiboNums =
  Table("n", 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233)

Then in your forAll simply call the function and compare it with the expected return value, like this:

forAll (first14FiboNums) { n =>
  FiboGen.next should equal (n)
}

Testing mutable objects

If you need to test a mutable object, one way you can use tables is to specify state transitions in a table. For example, imagine you wanted to test this mutable Counter class:

class Counter {
  private var c = 0
  def reset() { c = 0 }
  def click() { c += 1 }
  def enter(n: Int) { c = n }
  def count = c
}

A Counter keeps track of how many times its click method is called. The count starts out at zero and increments with each click invocation. You can also set the count to a specific value by calling enter and passing the value in. And the reset method returns the count back to zero. You could define the actions that initiate state transitions with case classes, like this:

abstract class Action
case object Start extends Action
case object Click extends Action
case class Enter(n: Int) extends Action

Given these actions, you could define a state-transition table like this:

val stateTransitions =
  Table(
    ("action", "expectedCount"),
    (Start,    0),
    (Click,    1),
    (Click,    2),
    (Click,    3),
    (Enter(5), 5),
    (Click,    6),
    (Enter(1), 1),
    (Click,    2),
    (Click,    3)
  )

To use this in a test, simply do a pattern match inside the function you pass to forAll. Make a pattern for each action, and have the body perform that action when there's a match. Then check that the actual value equals the expected value:

val counter = new Counter
forAll (stateTransitions) { (action, expectedCount) =>
  action match {
    case Start => counter.reset()
    case Click => counter.click()
    case Enter(n) => counter.enter(n)
  }
  counter.count should equal (expectedCount)
}

Testing invalid argument combinations

A table-driven property check can also be helpful to ensure that the proper exception is thrown when invalid data is passed to a method or constructor. For example, the Fraction constructor shown above should throw IllegalArgumentException if Integer.MIN_VALUE is passed for either the numerator or denominator, or zero is passed for the denominator. This yields the following five combinations of invalid data:

nd
Integer.MIN_VALUEInteger.MIN_VALUE
a valid valueInteger.MIN_VALUE
Integer.MIN_VALUEa valid value
Integer.MIN_VALUEzero
a valid valuezero

You can express these combinations in a table:

val invalidCombos =
  Table(
    ("n",               "d"),
    (Integer.MIN_VALUE, Integer.MIN_VALUE),
    (1,                 Integer.MIN_VALUE),
    (Integer.MIN_VALUE, 1),
    (Integer.MIN_VALUE, 0),
    (1,                 0)
  )

Given this table, you could check that all invalid combinations produce IllegalArgumentException, like this:

forAll (invalidCombos) { (n: Int, d: Int) =>
  an [IllegalArgumentException] should be thrownBy {
    new Fraction(n, d)
  }
}

Next, learn about generator-driven property checks.

ScalaTest is brought to you by Bill Venners and Artima.
ScalaTest is free, open-source software released under the Apache 2.0 license.

If your company loves ScalaTest, please consider sponsoring the project.

Copyright © 2009-2024 Artima, Inc. All Rights Reserved.

artima