Table Driven Testing in Scala

Reading Time: 3 minutes

As a software developer, I ensure to write unit tests for the code which I write (and at times the missing unit tests for the existing code). My aim has been to ensure to cover all the cases that arise out of the code written and therefore I face challenges in this too. Because of my endeavour to write a sufficiently strong test suite for the application, I often find repetition of steps across multiple test cases. The way I come across this problem is by using table driven tests provided by ScalaTest. Therefore, in this blog post, we will explore the use of table driven tests.

For instance, notice how the below code can result into verbose test cases and also redundant code across them.

Order.scala

case class Order(quantity: Int, price: Int) {
  def isEmptyOrder: Boolean = quantity == 0 && price == 0
}

OrderValidation.scala

object OrderValidation {

  def validateOrder(order: Order): Boolean =
    order.isEmptyOrder || (validatePrice(order.price) && validateQuantity(order.quantity))

  private def validatePrice(p: Int): Boolean = p > 0
  private def validateQuantity(q: Int): Boolean = q > 0

}

OrderValidationSpec.scala

import org.scalatest._
import org.scalatest.FlatSpec

class OrderValidationSpec extends FlatSpec with Matchers {

  "Order validation" should "validate and return true if it is an empty order" in {
    val emptyOrder = Order(0, 0)

    val orderValidation = OrderValidation
    orderValidation.validateOrder(emptyOrder) shouldBe true
  }

  "Order validation" should "validate and return true if positive quantity and price" in {
    val validOrder = Order(10, 10)
    val orderValidation = OrderValidation

    orderValidation.validateOrder(validOrder) shouldBe true
  }

  "Order validation" should "validate and return false if price is negative" in {
    val invalidOrder = Order(quantity = 10, price = -2)

    val orderValidation = OrderValidation
    orderValidation.validateOrder(invalidOrder) shouldBe false
  }

  "Order validation" should "validate and return false if quantity is negative" in {
    val invalidOrder = Order(quantity = -10, price = 2)

    val orderValidation = OrderValidation
    orderValidation.validateOrder(invalidOrder) shouldBe false
  }

  "Order validation" should "validate and return false if quantity and price are negative" in {
    val invalidOrder = Order(quantity = -10, price = -2)

    val orderValidation = OrderValidation
    orderValidation.validateOrder(invalidOrder) shouldBe false
  }
}

So, if we notice in the above example, there is a lot of boiler plating in the test code. For instance, the last three test cases only vary in the value of either quantity or price or both. The result is the same, false in all three cases.

So if we carefully analyse our 5 test cases, they basically form the given table:

QuantityPriceValidation Result
ZeroZerotrue
PositivePositivetrue
Non-negativeNegativefalse
NegativeNon-negativefalse
NegativeNegativefalse

If we can simply replace all our tests with the above table rather than writing those verbose cases, it saves a lot of boiler plating and consequently improves the readability of the test code. This is where table driven testing comes in.

To be able to use table driven tests, we need to mixin the trait called TableDrivenPropertyChecks.

OrderValidationTableDrivenSpec.scala

import org.scalatest.FreeSpec
import org.scalatest._
import org.scalatest.prop.TableDrivenPropertyChecks

class OrderValidationTableDrivenSpec extends FreeSpec with TableDrivenPropertyChecks with Matchers {

  "Order Validation" - {
    "should validate and return false if" - {
      val orders = Table(
        ("statement"                          , "order")
        , ("price is negative"                , Order(quantity = 10, price = -2))
        , ("quantity is negative"             , Order(quantity = -10, price = 2))
        , ("price and quantity are negative"  , Order(quantity = -10, price = -2))
      )

      forAll(orders) {(statement, invalidOrder) =>
        s"$statement" in {
          OrderValidation.validateOrder(invalidOrder) shouldBe false
        }
      }
    }
  }
}

Above in the new test class, I show how the last three cases of the above table have been clubbed together using table driven testing and therefore have reduced code redundancy. Besides, smart use of table driven testing can also allow us to reduce or in some cases, completely avoid the repetitions in the description of the test cases. And most importantly, it improves the readability of the test cases and therefore makes it easier for other developers to understand the code logic by following the test cases.

You can use table driven test cases with any testing style of ScalaTest but with my experience, I feel they are best used with WordSpec and FreeSpec as they easily help in avoiding repetitive descriptions.

Here is the sbt shell output which still shows the output of the every test row separately.

[info] Done compiling.
[info] OrderValidationTableDrivenSpec:
[info] Order Validation
[info]   should validate and return false if
[info]   - price is negative
[info]   - quantity is negative
[info]   - price and quantity are negative
[info] Run completed in 548 milliseconds.
[info] Total number of tests run: 3
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 3, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.

As an exercise, the readers can attempt to expand the test cases and include the first two cases of the table also in tests.

Key Points

  1. Table Driven tests help in reduction of redundant code in test cases
  2. Simply need to mixin TableDrivenProperyChecks
  3. Work with any ScalaTest testing style

Thank you for reading the blog. In further blogs, we will explore more salient features of ScalaTest.

Further reading – http://www.scalatest.org/user_guide/table_driven_property_checks


Knoldus-blog-footer-image

Written by 

Sonu Mehrotra is a Software Developer with industry experience of 5+ years. He is a Clean Code practitioner and an Agile follower. Sonu has helped financial institutions develop trading systems that make trading activities easier and more accessible to customers.