Core Concepts
Spek provides two built-in testing styles, but before proceeding to that - it is important to have a good grasp on some core concepts.
Structure¶
Tests are written using nested lambdas, each scope (level) can either be a group
or a test
.
object MyTest: Spek({ group("a group") { test("a test") { ... } group("a nested group") { test("another test") { ... } } } })
Understanding the difference between each scope is crucial in writing correct tests.
- Test scope is where you place your assertions/checks (in JUnit dialect - this is your test method).
- Group scope is used to organize your tests. It can contain test scopes and other group scopes as well. A very important thing to note is that group scopes will be eagerly executed during the discovery phase. (more about this on the next section).
Phases¶
Discovery¶
The goal of this phase is to build the test tree which outlines how the tests are executed. In order to achieve this, Spek will execute all group scopes starting from the root. Consider the following test:
object MyTest: Spek({ println("this is the root") group("some group") { println("some group") test("some test") { println("some test") } } group("another group") { println("another group") test("another test") { println("another test") } } })
this the root some group another group some test another test
Execution¶
In this phase the tests are executed. Spek traverses the test tree starting from the root and for each scope it will execute the registered fixtures (explained in the next section).
Fixtures¶
Fixtures are used to execute a piece of code before or after each scope, this is the recommended way to do any test setup.
object MyTest: Spek({ beforeGroup { println("before root") } group("some group") { beforeEachTest { println("before each test") } test("some test") { println("some test") } test("another test") { println("another test") } afterEachTest { println("after each test") } } afterGroup { println("after root") } })
The output of the test above is (lines are highlighted to differentiate lines printed in fixtures):
before root before each test some test after each test before each test another test after each test after root
Scope values¶
As a best practice you typically want test values to be unique for each test this can be done by using a lateinit
variable
and assigning it within a beforeEachTest
.
lateinit var calculator: Calculator beforeEachTest { calculator = Calculator() }
To make it more concise, Spek provides memoized
to do the same thing:
val calculator by memoized { Calculator() }
memoized
should not be used to hold the result of an action, use a fixture instead.
// BAD val item by memoized { 1 } val added by memoized { list.add(item) } // GOOD val item by memoized { 1 } lateinit var added: Boolean beforeEachTest { added = list.add(item) }
Caching modes¶
You can pass in an optional parameter to memoized
which controls how the values are cached.
CachingMode.TEST
: each test scope will receive a unique instance, this is the default.CachingMode.GROUP
: each group scope will receive a unique instance.CachingMode.SCOPE
: effectively a singleton.CachingMode.INHERIT
: internal use only.
If you are using the gherkin style note that the default caching mode is CachingMode.GROUP
.
Share common logic between tests¶
Often there is some code, which needs to be called repeatedly before and after tests.
To have code not duplicated through the code base, Spek allows sharing common logic through the use of Kotlin's extension functions.
Shared setup code can be implemented by declaring an extension function on the Root
interface.
This function is accessible in every test class or object, which derives from Spek
.
The call to the setup()
function is typically placed at the top.
The created objects can then be referenced by name.
// Put the common logic here fun Root.setup() { val obj by memoized( factory = { createObj() }, destructor = { it.dispose() } ) } object Test: Spek({ setup() // Fetches the object keyed by name 'obj' val obj: Object by memoized() })