Using tuples with many elements could be hard to read. It becomes harder in a collection. Table-driven property checks helps you to avoid duplicated code when you need to write the same tests with different values. You could have the expected output in the table, but I don't think it will be easier to read or maintain.
The article Knoldus - Table Driven Testing in Scala have a good example where instead of having the expected output in the table, it write tests that return the same value with different inputs:
Order.scala (model)
case class Order(quantity: Int, price: Int) {
def isEmptyOrder: Boolean = quantity == 0 && price == 0
}
OrderValidation.scala (logic)
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
}
OrderValidationTableDrivenSpec.scala (test)
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
}
}
}
}
}
In this case, the table just have the name of the case that is being validated and then the input value, which is the Order
in this case.
You can see the same approaach in the gist davidallsopp - PropertyTests.scala:
class Example extends PropSpec with PropertyChecks with ShouldMatchers {
val examples =
Table(
"set",
BitSet.empty,
HashSet.empty[Int],
TreeSet.empty[Int])
// table-driven
property("an empty Set should have size 0") {
//forAll(examples) { set => set.size should be(0) }
forAll(examples) { _.size should be(0) }
}
// table-driven, expecting exceptions
property("invoking head on an empty set should produce NoSuchElementException") {
forAll(examples) { set =>
a[NoSuchElementException] should be thrownBy { set.head }
}
}
// table-driven, expecting exceptions, with an alternative syntax
property("again, invoking head on an empty set should produce NoSuchElementException") {
forAll(examples) { set =>
evaluating { set.head } should produce[NoSuchElementException]
}
}
// A 2-column table
val colours = Table(
("Name", "Colour"), // First tuple is the title row
("r", Color.RED),
("g", Color.GREEN),
("b", Color.BLUE))
property("colours") {
forAll(colours) { (name: String, colour: Color) =>
colour.toString should include(s"$name=255") // e.g. java.awt.Color[r=0,g=255,b=0]
}
}
// A bit more concise with a case statement (don't need explicit types)
property("colours2") {
forAll(colours) { case (name, colour) => colour.toString should include(s"$name=255") } // e.g. java.awt.Color[r=0,g=255,b=0]
}
}
Each test case have different inputs but all of them expect the same output.
From the example provided in your question
val inputs = Table(
("a", "b"),
( 1, 2),
( -1, 2),
)
forAll(inputs) { (a, b) =>
sumFn(a, b) should equal (a + b)
diffFn(a, b) should equal (a - b)
}
It doesn't make sense to use Table-driven property checks (I think it was just a dummy example to avoid complexity). If your real case is similar to something like that, it could be better to validate the logic of your code using Generator-driven property checks. This type of tests is useful when you need to validate some properties, such as commutative property.
// Generator-driven property
forAll { (a: Int, b: Int) =>
(a + b) should be(b + a)
}
forAll { (a: Int, b: Int) =>
whenever(a != b && (a != 0 || b != 0)) {
(a - b) should not be(b - a)
}
}
Table-driven and Generator-driven property checks comes from Property-based testing.