Programming LambdaTest

Programming LambdaTest

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.

blog comments powered by Disqus

Ensure the success of your project

47 Degrees can work with you to help manage the risks of technology evolution, develop a team of top-tier engaged developers, improve productivity, lower maintenance cost, increase hardware utilization, and improve product quality; all while using the best technologies.