Thursday, October 29, 2009

ScalaCheck is Neat-o!

ScalaCheck is a testing library for Scala (and Java!). It is a step beyond just writing tests, you actually assert properties of some code and it generates a number of tests dynamically to test it.

A recent example of using this came from this StackOverflow question. It required a largish number of test cases, so it seemed a perfect fit to try out ScalaCheck.

The code to test was:


package blevins.example

object BigIntEncoder {
val radix = 36

implicit def byteArrayToString(ba: Array[Byte]): String = {
new java.math.BigInteger(ba).toString(radix)
}

implicit def stringToByteArray(s: String): Array[Byte] = {
new java.math.BigInteger(s, radix).toByteArray
}

}


To test, you write something like this:


package blevins.example

import org.scalacheck._

object BigIntEncoderSpecification extends Properties("BigIntEncoder") {
import Prop._
import Gen._

def genArray( fill: Array[Byte] => Unit) = Gen.sized { size =>
val bytes: Array[Byte] = new Array[Byte](size)
fill(bytes)
bytes
} suchThat (_.size > 0)

def filledByteArray( i: int ) = genArray(bytes =>
for (i <- 0 to bytes.length - 1) { bytes(i) = i.asInstanceOf[Byte] }
)

val arbByteArray = genArray(bytes => scala.util.Random.nextBytes(bytes))

val zeroByteArray = filledByteArray( 0x00 )

val fullByteArray = filledByteArray( 0xff )

def checkEncodingRoundTrip = { (ba: Array[Byte]) =>
import BigIntEncoder._
ba deepEquals stringToByteArray(byteArrayToString(ba))
}

property("random") = forAll(arbByteArray)(checkEncodingRoundTrip)
property("zero") = forAll(zeroByteArray)(checkEncodingRoundTrip)
property("full") = forAll(fullByteArray)(checkEncodingRoundTrip)
}



This code will generate random, all-zero, and all-binary-ones (0xff) arrays and test their equality using the checkEncodingRoundTrip method, which exercises the code to be tested. It failed sometimes for the random array, and failed all the time for the other two types. I also noticed that the random array failed when the first byte of the arrays was either 0x00 or 0xff. I assumed that this was a side effect of BigInteger taking the two-complement of the byte array, so I padded it like so:


package blevins.example

object BigIntEncoder {
val radix = 36

implicit def byteArrayToString(ba: Array[Byte]): String = {
new java.math.BigInteger(addByte(ba)).toString(radix)
}

implicit def stringToByteArray(s: String): Array[Byte] = {
stripByte(new java.math.BigInteger(s, radix).toByteArray)
}

def addByte(ba: Array[Byte]): Array[Byte] = {
val h = new Array[Byte](1)
h(0) = 0x01
h ++ ba
}

def stripByte(ba: Array[Byte]): Array[Byte] = {
ba.slice(1,ba.size)
}

}


This produced the desired results (all passing), but a large amount of output. I wrote my own output-generator like this:


package blevins.example

object Test extends Application {
import org.scalacheck._
import org.scalacheck.Test._

val params = Params(5000, 10, 1, 100, new java.util.Random(), 1, 1)
val props = checkProperties(BigIntEncoderSpecification, params)
for ((name, result) <- props) {
println(name + ": " + result.status)
}

}



This also increased the number of tests-per-property from 100 to 5000.
My output was:

BigIntEncoder.random: Passed
BigIntEncoder.zero: Passed
BigIntEncoder.full: Passed


Yay! Now go watch TV.

Reflective Calls in Scala

Scala 2.8 contains two experimental features to reflectively invoke methods. Are these cool?


import scala.reflect.RichClass._

val hello = "hello"
val helloStartsWith = classOf[String].reflectiveCall(hello, "startsWith")
val b1: Boolean = helloStartsWith("hell")
val b2: String = helloStartsWith("heaven") // ClassCastException (Boolean->String)
val b3 = helloStartsWith("blah") // ClassCastException (Boolean->Nothing)


import scala.reflect.Invocation._

val team: AnyRef = "team"
val any = team o 'contains("I") // returns 'Any'
val b4: Boolean = team oo 'contains("I") // ok
val b5 = team oo 'contain("I") // ClassCastException (Boolean->Nothing)



The methods called using "o" return "Any". Both reflectiveCall(...) and the "oo" version return the inferred type. Note that this doesn't relieve you from knowing the expected result type, or further reflecting it and casting. This is shown in the runtime errors with b2, b3, and b5.

EDIT: This post was accurate for the nightly 2.8 release when it was written, but this code appears to have been removed from the latest 2.8 betas.