47 Degrees joins forces with Xebia read more

Extending LambdaTest

Extending LambdaTest

This post examines an important feature of LambdaTest: its extensibility. Rather than trying to include all things for all needs, LambdaTest has a small core with a clean API that makes it easy to add new features.

LambdaTest was introduced in our 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 and can be found on GitHub here: LambdaTest.

Actions and States

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. In this post, we look at several examples of action definitions.

assertEq: A Builtin Action

We start with a built-in action, assertEq, that checks that two values are equal.

assertEq(i, 10, "Check i")

The code for assertEq can be found in the LambdaTest package object.

def assertEq[T](a: ⇒ T, b: ⇒ T, info: ⇒ String = "",
    showOk: Boolean = true, pos: String = srcPos()): LambdaAct = {
    SingleLambdaAct(t ⇒ try {
      val a1 = a
      val info0 = if (info == "") "" else s" ($info)"
      if (a1 == b) {
        val info1 = s"[$a1]$info0"
        if (showOk) t.success(info1, pos) else t
      } else {
        t.fail(s"[$a1 != $b]$info0", pos)
      }
    } catch {
      case ex: Throwable ⇒
        t.unExpected(ex, pos)
    })
  }

Actions are implemented in terms of a lower level API that consists of methods on the LambdaState class. In this example, t has type LambdaState and the low level methods are t.success, t.fail, and t.unexpected. The low-level API is used only to define actions and is not used directly within user testing.

assertRng: A New Action

It is easy to use the patterns seen in the actions in the package object to define new actions. Here we will define a new assertion that checks that a value is within a specified range.

assertRng(i, (2,7), "Check i") +
assertRng(s, ("a","d"), "Check s")

Here is code for the new action:

def assertRng[T <% Ordered[T]]
  (a: ⇒ T, b: ⇒ (T, T), info: ⇒ String = "",
   pos: String = srcPos()): LambdaAct = {
    SingleLambdaAct(t ⇒ try {
      val a1 = a
      val (low, high) = b
      val info0 = if (info == "") "" else s" ($info)"
      if (a1 < low) {
        t.fail(s"[$a1 < $low]$info0", pos)
      } else if (a1 > high) {
        t.fail(s"[$a1 > $high]$info0", pos)
      } else {
        val info1 = s"[$a1]$info0"
        t.success(info1, pos)
      }
    } catch {
      case ex: Throwable ⇒
        t.unExpected(ex, pos)
    })
  }

assertRng1: An Action Based on Other Actions

Actions can also be defined using other actions. Here we show another way of doing range checking:

def assertRng1[T <% Ordered[T]]
  (a: ⇒ T, b: ⇒ (T, T), info: ⇒ String = "",
   pos: String = srcPos()): LambdaAct = {
    assert(a > b._1, s"$info low", pos = pos) +
    assert(a < b._2, s"$info high", pos = pos)
  }

Note how the position reported for the inner asserts will be the position of the call to assertRng1.

fileWrap: A Compound Wrapper Action

Both assertEq and assertRng are simple actions. A compound action is an action that can contain other actions. There are compound actions in the package object such a label and test.

Here we define a new compound action for working with files. We want to first open the file, next perform assertions on the content of the file, and finally close the file. In other Scala test systems, this is often handled with before and after hooks. Here we provide a better solution using a compound action. We refer to compound actions that have these three parts as wrappers.

Suppose we have file foo.text with the following content:

foo
bar
zap

We can test foo.txt like this:

fileWrap("foo.txt") { f =>
   test("test1") {
      assertEq(f.readLine(), "foo", "line1") +
      assertEq(f.readLine(), "bar", "line2")
   }
}

And finally here is the code that defines the compound action fileWrap wrapper:

import java.io.{ BufferedReader, FileReader }

def fileWrap(fileName: String)(body: BufferedReader ⇒ LambdaAct): LambdaAct = {
  SingleLambdaAct(t ⇒ {
    val br = new BufferedReader(new FileReader(fileName))
    try {
      body(br).eval(t)
    } finally {
      br.close()
    }
  })
}

Note the internal API eval method on class LambdaAct. Also note the BufferedReader is injected as a parameter into the body.

cond: A Compound Conditional Action

This compound action combines three other actions. The If action is run as a test. If it succeeds, the Then action is run as a test. If it fails, the Else action is run as a test. A use looks like this:

cond("Conditional Test")(
   If = assert(p1, "If test"),
   Then = assert(p2, "Then test"),
   Else = assert(p3,"Else test")
)

Here is the code for the cond action:

def cond(name: String)
        (If:  LambdaAct,
         Then: => LambdaAct = SingleLambdaAct(t => t),
         Else: => LambdaAct = SingleLambdaAct(t => t)): LambdaAct = {
    SingleLambdaAct(t => {
      val t1 = test(s"$name:if")(If).eval(t)
      if (t1.reporter.failed == t.reporter.failed) {
        test(s"$name:then")(Then).eval(t1)
      } else {
        test(s"$name:else")(Else).eval(t1)
      }
    }
    )
}

To Learn More

See the complete LambdaTest documentation for more information 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.

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.