At the heart of boon, is a Predicate. A Predicate is a boolean expression. The most basic way of creating a Predicate in boon, is by testing two values for equality:
actualValue =?= expectedValue
The =?=
operator is a typesafe equality operator. See Operators for additional operators.
For example to test two Int operands for equality:
1 =?= 1
A Predicate with a description is an Assertion:
actualValue =?= expectedValue | "description"
For example:
1 =?= 1 | "one is one"
Assertions are first class constructs in boon and can be combined with the and
method to give even more Assertions:
1 =?= 1 | "Int equality" and
"Hello" =?= "Hello" | "String equality" and
true =?= true | "Boolean equality"
Assertions are grouped into a Test:
test("equality of things") {
1 =?= 1 | "Int equality" and
"Hello" =?= "Hello" | "String equality" and
true =?= true | "Boolean equality"
}
Tests are grouped into a Suite. All Suites must follow these rules:
- Must be an
object
- This prevents inheritance abuse - Extend
SuiteLike
- Override the
tests
method
Here is an outline of a simple Suite:
object MySuite extends SuiteLike("Stuff I want to test") {
val test1 =
val test2 =
..
val testn =
override def tests = oneOrMore(test1, test2 ... testn)
}
A more complete example:
package example
import boon._
object MyFirstSuite extends SuiteLike("Simple Stuff") {
private val t1 = test("equality of things") {
1 =?= 1 | "Int equality" and
"Hello" =?= "Hello" | "String equality" and
true =?= true | "Boolean equality"
}
override def tests = oneOrMore(t1)
}
As shown above, to define a Suite, you need the following import:
import boon._
The tests
method on SuiteLike
is defined as:
def tests: NonEmptySeq[Test]
NonEmptySeq
is a collection that must have at least one element. It is similar to NonEmptyList
in Cats and Scalaz. It can be constructed using the oneOrMore
function for when you have one or more tests. Requiring a NonEmptySeq
when defining a Suite means that you can't create a Suite without any tests. This is something that is possible in other testing frameworks and leads to confusion when an empty Suite passes (since it has no tests). boon strives to ensure that invalid states can't be represented.
Running the above Suite produces the following output:
Let's update our String equality Predicate so it fails:
"Hello" =?= "Yello" | "String equality"
Now when we run the Suite it produces the following output:
Any Boolean
expression can be turned into a Predicate. That Predicate can then be made an Assertion:
List.empty[String].isEmpty | "empty List is empty"
The main difference is that if the Assertion fails you get a Boolean failure not a type-specific failure:
[info] - empty List is empty [✗]
[info] => false != true
If you want to run multiple Assertions on an expression you can do so with the %@
operator:
%@(List(1, 2, 3, 4, 5)) { list =>
list.sum =?= 15 | "list sum" and
list.take(2) =?= List(1,2) | "list take"
}
In addition each block can be given a prefix:
%@(List(1, 2, 3, 4, 5), "list") { list =>
list.sum =?= 15 | "sum" and
list.take(2) =?= List(1,2) | "take"
}
results in the prefix prepended to each Assertion description:
[info] - list.sum [✓]
[info] - list.take [✓]
When nesting blocks with prefixes, lower blocks will have the prefix of each upper block prepended to their Assertions.
Sometimes when a test fails you want more information about the values of certain variables used to calculate the result. You can specify these values when creating an Assertion:
"Bilbo".contains("lbo") || "contains" |>
oneOrMore(
"subject" -> """"Bilbo"""",
"predicate" -> "contains",
"value" -> """"ob""""
) and
When the above Assertion fails, the contextual values supplied will be displayed:
[info] => false != true
[info] at .../StringSuite.scala:13
[info] #: subject -> "Bilbo"
[info] predicate -> contains
[info] value -> "ob"
Notice the use of the double pipes (||
) when adding a context (|>
). The double pipes imply you have more information to supply in addition to a description.
By default, all Assertions are executed independently of each other. What this means is that a prior failing Assertion, will not prevent a subsequent Assertion from running:
val t1 = test("equality of things") {
1 =?= 2 | "Int equality" and
"Hello" =?= "Hello" | "String equality" and
true =?= true | "Boolean equality"
}
Results in:
[info] - equality of things [failed]
[info] - Int equality [✗]
[info] => 1 != 2
[info] at .../MyFirstSuite.scala:8
[info] - String equality [✓]
[info] - Boolean equality [✓]
Notice that although, the Int equality Assertion failed, the other Assertions completed successfully.
Continue-On-Failure Assertions are shown with a '-' in the output.
Although unnecessary, you could explicitly define a Test as Continue-On-Failure with the continueOnFailure
method:
val t1 = test("equality of things") {
continueOnFailure(
1 =?= 2 | "Int equality" and
"Hello" =?= "Hello" | "String equality" and
true =?= true | "Boolean equality"
)
}
What if we didn't want to run any of the other Assertions after a failing Assertion? We could specify that by using the stopOnFailuire
method:
val t1 = test("equality of things") {
stopOnFailure(
1 =?= 2 | "Int equality" and
"Hello" =?= "Hello" | "String equality" and
true =?= true | "Boolean equality"
)
}
If we ran the above, it would fail with:
[info] - equality of things [failed]
[info] ↓ Int equality [✗]
[info] => 1 != 2
[info] at .../MyFirstSuite.scala:8
[info] ↓ String equality (not run)
[info] ↓ Boolean equality (not run)
Notice that the String equality and Boolean equality Assertions did not run after the Int equality Assertion failed.
Stop-On-Failure Assertions are shown with a '↓' symbol in the output.
If you have a truth table of inputs against some expected output, you can create a tabulated test. Start off by creating a truthTable
:
val multTable =
truthTable(
(1, 4) -> tval(4),
(2, 6) -> tval(12),
(5, 10) -> tval(50),
(7, 7) -> tval(49),
(-2, -1) -> tval(2),
(10, 20) -> tval(200)
)
multTable
defines a truth table that takes two Ints
and produces an expected result which is also an Int
(tval
).
You can then use the truth table within a table
test:
val multTest = table[(Int, Int), Int]("Multiplication", multTable)(n => n._1 * n._2)
When we run the multTest we get:
- Multiplication [passed]
- with (1, 4) is 4 [✓]
- with (2, 6) is 12 [✓]
- with (5, 10) is 50 [✓]
- with (7, 7) is 49 [✓]
- with (-2, -1) is 2 [✓]
- with (10, 20) is 200 [✓]
Say you had a collection of values and you wanted to assert that they all held some kind of property. You could just map over those values with your Assertion and get one big Assertion that verifies it all.
val t1 = test("less than 10") {
oneOrMore(1, (2 to 9):_*).map(n => n < 10 | s"$n < 10")
}
When we run the above Test we get:
- less than 10 [passed]
- 1 < 10 [✓]
- 2 < 10 [✓]
- 3 < 10 [✓]
- 4 < 10 [✓]
- 5 < 10 [✓]
- 6 < 10 [✓]
- 7 < 10 [✓]
- 8 < 10 [✓]
- 9 < 10 [✓]
note: This works with NonEmptySeq
and any scala.collection.Iterable
The default way of combining Assertions is by the Continue-On-Failure model. The above is just a short-hand for:
val t1 = test("less than 10") {
continueOnFailure(oneOrMore(1, (2 to 9):_*).map(n => n < 10 | s"$n < 10"))
}
Similarly we can combine Assertions with Stop-On-Failure:
val t1 = test("less than 10") {
stopOnFailure(oneOrMore(1, (2 to 9):_*).map(n => n < 10 | s"$n < 10"))
}
Sometimes you want to run an Assertion on a couple of different types. You can't use the type safe equal operator (=?=
) as it expects the same types.
For example to run an Assertion on a String
and an Int
, you just need to provide a Assertion
function that compares the two values:
(valueOfType1, valueOfType2) =>= ((v1, v2) => Assertion)
For example to Assert that the length of "Hello World" is 11:
("Hello World", 11) =>= ((a, b) => a.length =?= b | "greet length")
When we run the above we get:
- greet length [✓]
or when it fails we get:
- greet length [✗]
=> 11 != 12
at ...
#: values -> ("Hello World", 12)