Programming LambdaTest
by John Nestor
- •
- March 20, 2017
- •
- scala• lambdatest• open source• scala• functional programming• sbt• testing
- |
- 5 minutes to read.

This post examines an important feature of LambdaTest: its programmability. Traditionally, unit tests are long boring sequences of simple tests. We believe that testing is a task for programmers (and one of the most important programming activities). The goal should be to make tests as comprehensive as possible and do it in a highly leveraged way (where a small amount of test code has maximum value). In this post, we look at two of LambdaTest’s programming mechanisms: ScalaCheck and test generation.
LambdaTest was introduced in a previous blog post Introducing LambdaTest. LambdaTest is a new small clean library for testing Scala code developed by 47 Degrees. Tests can be run either via SBT or directly. All code is open source with an Apache 2 license.
View LambdaTest on GitHub or visit the LambaTest microsite for more information.
ScalaCheck
ScalaCheck is an awesome library for doing automated property-based testing of Scala code. ScalaCheck can be used directly from LambdaTest (assertSC is just another kind of assertion).
Here are a few simple examples of using ScalaCheck inside LambdaTest.
import com.fortysevendeg.lambdatest._
import org.scalacheck.Prop._
class ScalaCheckTest extends LambdaTest {
def brokenReverse[X](xs: List[X]): List[X] =
if (xs.length > 4) xs else xs.reverse
def act = {
test("String Length") {
assertSC() {
forAll { s: String =>
s.length >= 0
}
}
} +
test("Abs") {
assertSC() {
forAll { x: Int =>
Math.abs(x) >= 0
}
}
} +
test("List") {
assertSC() {
forAll { (xs: List[Int]) =>
xs.length > 0 ==> (xs.last == brokenReverse(xs).head)
}
}
}
}
}
And here is the output from those tests.
***** running ScalaCheck Test
Test: String Length
Ok: Passed 100 tests (ScalaCheckTest.scala Line 10)
Test: Abs
Fail: Falsified after 8 passed tests.
> ARG_0: -2147483648 (ScalaCheckTest.scala Line 18)
Test: List
Fail: Falsified after 4 passed tests.
> ARG_0: List("0", "0", "0", "0", "1")
> ARG_0_ORIGINAL: List("-1627881204", "-2147483648", "0", "575454471", "1984585744") (ScalaCheckTest.scala Line 25)
***** ScalaCheck Test: 3 tests 2 failed 0.725 seconds
These examples only scratch the surface of the programmability of ScalaCheck. You can see these and other more powerful examples in the following excellent blog post: Practical ScalaCheck
LambdaTest API and Test Generation
LambdaTest has a very simple API that makes test generation very easy. Tests are generated by writing Scala code whose output is a LambdaTest action.
Tests are composed of actions (with type LambdaAct
).
An action is a function that transforms one testing state to a
new testing state.
Testing states (with type LambdaState
) are immutable.
Actions include assertions and grouping actions such as test
and label
.
Actions are composed in two ways. First, two Actions can be combined into
a single action using the infix +
operator. Second, compound actions
are actions that can contain other actions. The result is that a test class has an act
that is a tree of actions.
Unlike other test systems whose APIs are often a complex tangle of traits and
classes,
the LambdaTest API is based on the single class LambdaAct
.
The following two sections contain examples of test generation.
Test List
Here, tests are generated based on the elements of a list:
import com.fortysevendeg.lambdatest._
class TestList extends LambdaTest {
def checkList(name: String, s: List[Int]): LambdaAct = {
changeOptions(_.copy(onlyIfFail = true)) {
s.zipWithIndex.map {
case (i, j) => {
test(s"Test list element $name($j)") {
assertEq(i, j)
}
}
}
}
}
val s1 = List(0, 5, 6, 3)
val s2 = List(0, 1, 2, 3, 3, 5, 5)
val act = {
checkList("s1", s1) +
checkList("s2", s2)
}
}
The changeOption
action turns off output for any tests that succeed.
The body of changeOptions
has type List[LambdaAct]
. There is an implicit
conversion that converts that to LambdaAct
.
Here is the output:
***** running Test List
Test: Test list element s1(1)
Fail: [5 != 1] (TestList.scala Line 10)
Test: Test list element s1(2)
Fail: [6 != 2] (TestList.scala Line 10)
Test: Test list element s2(4)
Fail: [3 != 4] (TestList.scala Line 10)
Test: Test list element s2(6)
Fail: [5 != 6] (TestList.scala Line 10)
***** Test List: 11 tests 4 failed 0.018 seconds
Test Tree
Here assertions are generated by recursively walking a tree:
import com.fortysevendeg.lambdatest._
class TestTree extends LambdaTest {
trait Tree
case class Inner(left: Tree, Right: Tree) extends Tree
case class Leaf(v: Int) extends Tree
def checkTree1(t: Tree, pos: String = ""): LambdaAct = {
t match {
case Inner(l, r) =>
checkTree1(l, pos = pos + "L") +
checkTree1(r, pos = pos + "R")
case Leaf(v) =>
assert(v > 0, s"Test leaf $pos = $v")
}
}
def checkTree(name: String, t: Tree): LambdaAct = {
test(name)(checkTree1(t))
}
val t = Inner(Inner(Leaf(2), Leaf(-3)), Leaf(0))
val act = checkTree("Test Tree", t)
}
Here is the output:
***** running Test Tree
Test: Test Tree
Ok: Test leaf LL = 2 (TestTree.scala Line 15)
Fail: Test leaf LR = -3 (TestTree.scala Line 15)
Fail: Test leaf R = 0 (TestTree.scala Line 15)
***** Test Tree: 1 tests 1 failed 0.008 seconds
To Learn More
See the LambdaTest documentation on GitHub for complete documentation and lots of examples.
And keep an eye out for future blog posts covering some of the more advanced features of LambdaTest and follow @47deg.