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

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.