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.

1 comment:

Anonymous said...

Your blog keeps getting better and better! Your older articles are not as good as newer ones you have a lot more creativity and originality now keep it up!