Overview
This guide assumes that you have a basic knowledge of Kotlin. If you have a Ruby background and are familiar with RSpec, you won’t have any issues to get up and running.
What is Spek?
Tests are not only to check the code you’ve written executes and works, but also that it works as it should, that is, that as a developer what’s been implemented matches the requirements.
A green test doesn’t tell you that the code correctly implements the specification. The user does, once they try it. However, we can try and make sure we’ve correctly understood the specifications by contrasting the code with the specs. And what better place to have these specs to lookup than in the same place we write the test code?
That’s what Spek does. It is a specification framework that allows you to easily define specifications in a clear, understandable, human readable way.
You call them tests, we call them specifications.
Why Kotlin?
Kotlin is statically typed OSS language developed and sponsored by JetBrains. It offers many advantages over Java such as its conciseness, expressiveness and the ability to create nice DSL. It’s 100% compatible with Java which means you can call Java from Kotlin and Kotlin from Java. And it’s also easy to pick up.
Spek is written in Kotlin and specifications you write will be written in Kotlin. However, your SUT (System under Test) can be Java or Kotlin. As such you can easily use Kotlin for your specifications. Yes, you call them tests. Start calling them specifications.
Supported Java Versions
Current version requires JDK 8 (as a requirement for JUnit 5).
Android
Android integration tests is currently not supported and you have to use the Jack toolchain to be able to build against Java 8.
IDE Support
Spek provides an IntelliJ IDEA and Android Studio plugin.
Important
|
If you’re using the JUnit 4 runner, the plugin won’t work as expected due to the bundled JUnit plugin taking over. |
Setting up
Currently, Spek is implemented as a JUnit Platform TestEngine.
You will need to include org.jetbrains.spek:spek-api
and org.jetbrains.spek:spek-junit-platform-engine
in your test classpath. The former is needed during compilation, while the latter during runtime only.
Note
|
For Android projects check Using Spek with JUnit 4 section. |
Gradle
JUnit Platform provides a gradle plugin.
// setup the plugin
buildscript {
dependencies {
classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0'
}
}
apply plugin: 'org.junit.platform.gradle.plugin'
junitPlatform {
filters {
engines {
include 'spek'
}
}
}
repositories {
maven { url "http://dl.bintray.com/jetbrains/spek" }
}
// setup dependencies
dependencies {
testCompile 'org.jetbrains.spek:spek-api:1.1.5'
testRuntime 'org.jetbrains.spek:spek-junit-platform-engine:1.1.5'
}
Gradle Kotlin Script
import org.gradle.api.plugins.ExtensionAware
import org.junit.platform.gradle.plugin.FiltersExtension
import org.junit.platform.gradle.plugin.EnginesExtension
import org.junit.platform.gradle.plugin.JUnitPlatformExtension
// setup the plugin
buildscript {
dependencies {
classpath("org.junit.platform:junit-platform-gradle-plugin:1.0.0")
}
}
apply {
plugin("org.junit.platform.gradle.plugin")
}
configure {
filters {
engines {
include("spek")
}
}
}
// setup dependencies
dependencies {
testCompile("org.jetbrains.spek:spek-api:1.1.5")
testRuntime("org.jetbrains.spek:spek-junit-platform-engine:1.1.5")
}
// extension for configuration
fun JUnitPlatformExtension.filters(setup: FiltersExtension.() -> Unit) {
when (this) {
is ExtensionAware -> extensions.getByType(FiltersExtension::class.java).setup()
else -> throw Exception("${this::class} must be an instance of ExtensionAware")
}
}
fun FiltersExtension.engines(setup: EnginesExtension.() -> Unit) {
when (this) {
is ExtensionAware -> extensions.getByType(EnginesExtension::class.java).setup()
else -> throw Exception("${this::class} must be an instance of ExtensionAware")
}
}
Using later versions of Kotlin
If you are using a version of Kotlin that is different than the one used by Spek, you will want to exclude the group in your dependency definition.
dependencies {
// your application's newer version of Kotlin
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
testCompile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testCompile ('org.jetbrains.spek:spek-api:1.1.5') {
exclude group: 'org.jetbrains.kotlin'
}
testRuntime ('org.jetbrains.spek:spek-junit-platform-engine:1.1.5') {
exclude group: 'org.junit.platform'
exclude group: 'org.jetbrains.kotlin'
}
}
Maven
JUnit provides support for Maven.
...
<dependency>
<groupId>org.jetbrains.spek</groupId>
<artifactId>spek-api</artifactId>
<version>1.1.5</version>
<type>pom</type>
</dependency>
Using Spek with JUnit 4
JUnit 4 based runner is still supported via JUnit Platform. You will need the following dependencies on the test classpath:
-
org.jetbrains.spek:spek-api:1.1.5
-
org.jetbrains.spek:spek-junit-platform-engine:1.1.5
-
org.junit.platform:junit-platform-runner:1.0.0
The finally, annotate your specs with @RunWith(JUnitPlatform::class)
.
@RunWith(JUnitPlatform::class)
class CalculatorSpec: Spek({
...
})
Important
|
As mentioned in the IDE Support section, the IDEA plugin won’t work if you’re using the JUnit 4 runner. |
Writing Specifications
Styles
It is important to know that Spek allows two styles of tests:
-
given, on, it
-
describe it
given, on, it
This is the classic Spek style and what the first version of Spek was based on. The idea is quite straightforward:
A test consists of four components:
-
name: which is the class name, indicating what exactly it is we’re testing
-
given: which defines the context, that is, the conditions under which I’m testing. Can be repeated any number of times for each
name
-
on: which defines the action, that is, what is executing for the test to later verify. Can be repeated any number of times for each
given
. -
it: which defines the actual tests, that is, what to be verified once the action is executed. Can be repeated any number of times for each
on
.
In the case of a calculator, this would be:
object CalculatorSpec: Spek({
given("a calculator") {
val calculator = SampleCalculator()
on("addition") {
val sum = calculator.sum(2, 4)
it("should return the result of adding the first number to the second number") {
assertEquals(6, sum)
}
}
on("subtraction") {
val subtract = calculator.subtract(4, 2)
it("should return the result of subtracting the second number from the first number") {
assertEquals(2, subtract)
}
}
}
})
Notice that both in this style, as the one below, the entire test is described inside of the Spek constructor. Each specification needs to inherit from the class Spek
defined
in the org.jetbrains.api
package.
describe, it
The style follows the Jasmine and Mocha style frameworks, where things are defined in describe
blocks.
A test consists of a minimum of three parts:
-
name: which is the class name, indicating what exactly it is we’re testing
-
describe: which defines the context of what we’re testing. We can nest as many
describes
as we want. Each one should provide more context -
it: which defines the actual tests, that is, what to be verified once the action is executed. Can be repeated any number of times for each
describe
object SimpleSpec: Spek({
describe("a calculator") {
val calculator = SampleCalculator()
on("addition") {
val sum = calculator.sum(2, 4)
it("should return the result of adding the first number to the second number") {
assertEquals(6, sum)
}
}
on("subtraction") {
val subtract = calculator.subtract(4, 2)
it("should return the result of subtracting the second number from the first number") {
assertEquals(2, subtract)
}
}
}
})
Notice that both in this style, as the one above, the entire test is described inside of the Spek constructor. Each specification needs to inherit from the class Spek
defined
in the org.jetbrains.api
package.
Assertion Framework
Spek doesn’t have any built-in assertions, luckily there are several libraries in the wild that do. Below are several suggestions:
Scopes
Spek provides 3 types of scopes, which are used to define your spec.
Test
Equivalent to test methods in JUnit, normally all assertions are done in this scope. Use it
to define a test scope.
Group
May contain an arbitrary number of nested scopes for better grouping of your tests.
Action
Represents an action that will be performed and may contain any number of test scopes. You can think of each test
as an assertion to
validate the expected behaviour of the action performed.
Basic Structure
Your specifications need to inherit from the org.jetbrains.spek.api.Spek
base class.
object SimpleSpec: Spek({
describe("a calculator") {
val calculator = SampleCalculator()
on("addition") {
val sum = calculator.sum(2, 4)
it("should return the result of adding the first number to the second number") {
assertEquals(6, sum)
}
}
on("subtraction") {
val subtract = calculator.subtract(4, 2)
it("should return the result of subtracting the second number from the first number") {
assertEquals(2, subtract)
}
}
}
})
Aside from describe
, Spek also provides given
and context
to create group
scopes.
Important
|
Due to how Spek is structured, group scopes are eagerly evaluated during the discovery phase. Any logic that
needs to be evaluated before and/or after test scopes should be done using fixtures, which will be discussed in the next section.
|
Fixtures
Spek allows running arbitrary code before and after a group
and test
is executed.
Note
|
You can’t declare fixtures within action scopes.
|
object FixtureSpec: Spek({
describe("a group") {
beforeGroup {
...
}
beforeEachTest {
...
}
context("a nested group") {
beforeEachTest {
...
}
beforeEachTest {
...
}
it ("should work") { ... }
}
it("do something") { ... }
afterEachTest {
...
}
afterGroup {
...
}
}
})
Ignoring tests
Each scope method have a variant prefixed with x
(e.g. xdescribe
, xit
, etc…), which Spek will ignore when executing the spec.
Subjects
Note
|
This feature is currently experimental. |
Normally you will only have a single class as the SUT. Spek provides an idiomatic way of writing this types of tests and removing a lot of boilerplate code.
object SimpleCalculatorSpec: SubjectSpek<Calculator>({
subject { Calculator() } (1)
it("should return the result of adding the first number to the second number") {
assertEquals(6, subject.sum(2, 4)) (2)
}
it("should return the result of subtracting the second number from the first number") {
assertEquals(2, subject.subtract(4, 2)) (2)
}
})
-
Tell Spek how to instantiate the subject, in this case a
Calculator
. This will be invoked for everytest
scope, which means eachtest
scope will have a unique instance. -
Use
subject
to access the instance of the subject.
Shared Subjects
This feature is useful when testing subclasses, removing the need of duplicating test code.
Given AdvancedCalculator
which is a subclass of Calculator
, a typical test will look like the following:
object AdvancedCalculatorSpec: SubjectSpek<AdvancedCalculator>({
subject { AdvancedCalculator() }
itBehavesLike(SimpleCalculatorSpec)
describe("pow") {
it("should return the power of base raise to exponent") {
assertEquals(subject.pow(2, 2), 4)
}
}
})
This will include all scopes declared in SimpleCalculatorSpec
.
FAQ
Q: What is Kotlin?
Kotlin is an Apache 2 OSS Language targetted at the JVM and JavaScript and is developed by JetBrains. It is aimed at being a concise modern language for general use. It also rocks!
Q: Is Spek a BDD or a TDD framework?
Spek in Dutch means Bacon, so you could think of it as a Bacon Driven Development framework. Being serious for a moment though, we (at least the original author) believe that there is a false distinction in frameworks around what TDD or BDD is. Unit tests are ultimately about defining the specifications of your system. As such, Spek is merely a specification framework if it can be called anything. For more information read What BDD has taught me.
Q: Can I have more than one on per given?
Yes you can. How you group your specifications is up to you.
Q: Can I have more than one it per on?
Yes you can. In real world applications, an action can lead to several reactions.
Q: Isn’t it bad to have more than one assertion per test?
Traditionally, in unit testing it’s been recommended that you should limit each test to one assertion with the idea that you test a single unit, and at the same time, find it easy to see where a test has failed. In Spek you can still comply with this guidance. You can have multiple it but you limit each to one assertion.
Q: Can I contribute?
Q: Is Spek an official JetBrains project?
No. Spek was a project initiated by Hadi Hariri and has multiple contributors both internally from JetBrains as well as externally. While it is hosted on the JetBrains account under GitHub, it is not a project that is officially supported.
Q: Is Spek the recommended way to test Kotlin code
Spek is just one of many options available to write tests, however any testing framework can be used with Kotlin, such as JUnit. Depending on your preferences, style and SUT, then Spek may or may not be the option for you.
Q: What is the licensing?
Spek is licensed under BSD.
Changelog
- Releases
-
- 1.1.0
-
-
Renamed
Dsl
toSpecBody
. -
Added support for extensions.
-
Reworked subjects as an extension.
-
Support specs defined as objects.
-
Added
beforeGroup
andafterGroup
fixtures. -
Changed
LifecycleAware
signature to allow local delegation.
-
- 1.0.0
-
-
Initial release of Spek.
-
Core Team
Hadi Hariri
Ranie Jade Ramiso
Artem Zinnatullin
Main Contributors
In alphabetical order
Hadi Hariri
Ranie Jade Ramiso
Laura Kogler
Eugene Petrenko
Ilya Ryzhenkov