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:
Quantity | Price | Validation Result |
Zero | Zero | true |
Positive | Positive | true |
Non-negative | Negative | false |
Negative | Non-negative | false |
Negative | Negative | false |
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
- Table Driven tests help in reduction of redundant code in test cases
- Simply need to mixin TableDrivenProperyChecks
- 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
