1. Introduction
Specification Testing frameworks are complementary to Unit Testing frameworks for testing our applications.
In this tutorial, we’ll introduce the Spek framework – a Specification Testing framework for Java and Kotlin.
2. What Is Specification Testing?
Simply put, in Specification Testing, we start with the specification and describe the intention of the software, instead of its mechanics.
This is often leveraged in Behavior Driven Development since the intention is to validate a system against predefined specifications of our application.
Commonly known Specification Testing frameworks include Spock, Cucumber, Jasmine, and RSpec.
2.1. What Is Spek?
Spek is a Kotlin-based Specification Testing framework for the JVM. It’s designed to work as a JUnit 5 Test Engine. This means that we can easily plug it into any project that already uses JUnit 5 to run alongside any other tests that we might have.
It’s also possible to run the tests using the older JUnit 4 framework, by using the JUnit Platform Runner dependency if needed.
2.2. Maven Dependencies
To use Spek, we need to add the required dependencies to our Maven build:
<dependency>
<groupId>org.jetbrains.spek</groupId>
<artifactId>spek-api</artifactId>
<version>1.1.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.spek</groupId>
<artifactId>spek-junit-platform-engine</artifactId>
<version>1.1.5</version>
<scope>test</scope>
</dependency>
The spek-api dependency is the actual API used for the testing framework. It defines everything that our tests will be working with. The spek-junit-platform-engine dependency is then the JUnit 5 Test Engine needed to execute our tests.
Note that all of the Spek dependencies need to be the same version as each other. The latest version can be found on here.
2.3. First Test
Once Spek is set up, writing tests is a simple case of writing the correct class in the correct structure. This is slightly unusual to make it more readable.
Spek requires that our tests all inherit from an appropriate superclass – typically Spek – and that we implement our tests by passing a block to the constructor of this class:
class FirstSpec : Spek({
// Implement the test here
})
3. Test Styles
Specification Testing emphasizes writing tests in a way that’s as readable as possible. Cucumber, for example, writes the entire test in human-readable language and then ties it to steps so that the code is kept separate.
Spek works by using special methods that act as readable strings, each of which is given a block to execute as appropriate. There are some variations on what functions we use depending on the way we want the tests to read.
3.1. given/on/it
One way that we can write our tests is in the “given/on/it” style.
This uses methods called given, on and it, nested in that structure, to write our tests:
- given – sets up the initial conditions for the test
- on – execute the test action
- it – assert that the test action performed correctly
We can have as many of each block as we need but must nest them in this order:
class CalculatorTest : Spek({
given("A calculator") {
val calculator = Calculator()
on("Adding 3 and 5") {
val result = calculator.add(3, 5)
it("Produces 8") {
assertEquals(8, result)
}
}
}
})
This test reads very easily. Focusing on the test steps, we can read it as “Given a calculator, On adding 3 and 5 It produces 8”.
3.2. describe/it
The other way that we can write our tests is in the “describe/it” style. Instead, this uses the method describe for all of the nestings, and keeps using it for our assertions.
In this case, we can nest the describe methods as much as we need to write our tests:
class CalculatorTest : Spek({
describe("A calculator") {
val calculator = Calculator()
describe("Addition") {
val result = calculator.add(3, 5)
it("Produces the correct answer") {
assertEquals(8, result)
}
}
}
})
There is less structure enforced on the tests using this style, meaning that we have a lot more flexibility in how we write the tests.
Unfortunately, the downside to this is that the tests don’t read as naturally as when we use “given/on/it”.
3.3. Additional Styles
Spek doesn’t enforce these styles, and it will allow for the keywords to be interchanged as much as desired. The only requirements are that all assertions exist inside it and that no other blocks are found at that level.
The full list of nesting keywords that are available is:
- given
- on
- describe
- context
We can use these to give our tests the best structure possible for how we want to write them.
3.4. Data-Driven Tests
The mechanism used for defining tests is all nothing more than simple function calls. This means that we can do other things with them, like any normal code. In particular, we can call them in a data-driven way if we so desire.
The easiest way to do this is to loop over the data that we want to use, and call the appropriate block from inside this loop:
class DataDrivenTest : Spek({
describe("A data driven test") {
mapOf(
"hello" to "HELLO",
"world" to "WORLD"
).forEach { input, expected ->
describe("Capitalising $input") {
it("Correctly returns $expected") {
assertEquals(expected, input.toUpperCase())
}
}
}
}
})
We can do all sorts of things like this if we need to, but this is likely the most useful.
4. Assertions
Spek doesn’t prescribe any particular way of using assertions. Instead, it allows us to use whatever assertion framework we’re most comfortable with.
The obvious choice will be the org.junit.jupiter.api.Assertions class, since we’re already using the JUnit 5 framework as our test runner.
However, we can also use any other assertion library that we want if it makes our tests better – e.g., Kluent, Expekt or HamKrest.
The benefit of using these libraries instead of the standard JUnit 5 Assertions class is down to the readability of the tests.
For example, the above test re-written using Kluent reads as:
class CalculatorTest : Spek({
describe("A calculator") {
val calculator = Calculator()
describe("Addition") {
val result = calculator.add(3, 5)
it("Produces the correct answer") {
result shouldEqual 8
}
}
}
})
5. Before/After Handlers
As with most test frameworks, Spek can also execute logic before/after tests.
These are, exactly as their name implies, blocks that are executed before or after the test itself.
The options here are:
- beforeGroup
- afterGroup
- beforeEachTest
- afterEachTest
These can be placed in any of the nesting keywords and will apply to everything inside that group.
The way Spek works, all code inside any of the nesting keywords is executed immediately on the start of the test, but the control blocks are executed in a particular order centered around it blocks.
Working from the outside-in, Spek will execute each beforeEachTest block immediately before every it block nested within the same group, and each afterEachTest block immediately after every it block. Equally, Spek will execute each beforeGroup block immediately before every group and each afterGroup block immediately after every group in the current nesting.
This is complicated, and is best explained with an example:
class GroupTest5 : Spek({
describe("Outer group") {
beforeEachTest {
System.out.println("BeforeEachTest 0")
}
beforeGroup {
System.out.println("BeforeGroup 0")
}
afterEachTest {
System.out.println("AfterEachTest 0")
}
afterGroup {
System.out.println("AfterGroup 0")
}
describe("Inner group 1") {
beforeEachTest {
System.out.println("BeforeEachTest 1")
}
beforeGroup {
System.out.println("BeforeGroup 1")
}
afterEachTest {
System.out.println("AfterEachTest 1")
}
afterGroup {
System.out.println("AfterGroup 1")
}
it("Test 1") {
System.out.println("Test 1")
}
}
}
})
The output of running the above is:
BeforeGroup 0
BeforeGroup 1
BeforeEachTest 0
BeforeEachTest 1
Test 1
AfterEachTest 1
AfterEachTest 0
AfterGroup 1
AfterGroup 0
Straight away we can see that the outer beforeGroup/afterGroup blocks are around the entire set of tests, whilst the inner beforeGroup/afterGroup blocks are only around the tests in the same context.
We can also see that all of the beforeGroup blocks are executed before any beforeEachTest blocks and the opposite for afterGroup/afterEachTest.
A larger example of this, showing the interaction between multiple tests in multiple groups, can be seen on GitHub.
6. Test Subjects
Many times, we will be writing a single Spec for a single Test Subject. Spek offers a convenient way to write this, such that it manages the Subject Under Test for us automatically. We use the SubjectSpek base class instead of the Spek class for this.
When we use this, we need to declare a call to the subject block at the outermost level. This defines the test subject. We can then refer to this from any of our test code as subject.
We can use this to re-write our earlier calculator test as follows:
class CalculatorTest : SubjectSpek<Calculator>({
subject { Calculator() }
describe("A calculator") {
describe("Addition") {
val result = subject.add(3, 5)
it("Produces the correct answer") {
assertEquals(8, result)
}
}
}
})
It may not seem like much, but this can help to make the tests a lot more readable, especially when there are a large number of test cases to consider.
6.1. Maven Dependencies
To use the Subject Extension, we need to add a dependency to our Maven build:
<dependency>
<groupId>org.jetbrains.spek</groupId>
<artifactId>spek-subject-extension</artifactId>
<version>1.1.5</version>
<scope>test</scope>
</dependency>
7. Summary
Spek is a powerful framework allowing for some very readable tests, which in turn means that all parts of the organization can read them.
This is important to allow all colleagues to contribute towards testing the entire application.
Finally, code snippets, as always, can be found over on GitHub.