Skip to content

Commit

Permalink
Add extra scopes for BehaviorSpec
Browse files Browse the repository at this point in the history
To improve BehaviorSpec customizability, this commit adds extra scopes to BehaviorSpec. As discussed, users can concatenate `And` indefinitely, but it's ok. As `And` is optional to complete a test. This behavior is also observed in `FreeSpec`, as it can concatenate infinite test scopes too.

To do that, every context (Given, And/When and Then) receive a new possibility of scoping, `And`. There are some variations of it, to build tests correctly: GivenAndContext, WhenAndContext and ThenAndContext. All of these are optional to use.

Fixes #562
  • Loading branch information
LeoColman committed Jan 18, 2019
1 parent 0f6801c commit 32afbe5
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 27 deletions.
Expand Up @@ -15,48 +15,156 @@ abstract class AbstractBehaviorSpec(body: AbstractBehaviorSpec.() -> Unit = {})
body()
}

fun Given(name: String, test: suspend GivenContext.() -> Unit) = addGivenContext(name, test)
fun given(name: String, test: suspend GivenContext.() -> Unit) = addGivenContext(name, test)

private fun addGivenContext(name: String, test: suspend GivenContext.() -> Unit) {
addTestCase("Given: $name", { thisSpec.GivenContext(this).test() }, defaultTestCaseConfig, TestType.Container)
}

@KotlinTestDsl
inner class GivenContext(val context: TestContext) {
suspend fun And(name: String, test: suspend WhenContext.() -> Unit) = and(name, test)
suspend fun and(name: String, test: suspend WhenContext.() -> Unit) = add("And: $name", test)
suspend fun When(name: String, test: suspend WhenContext.() -> Unit) = `when`(name, test)
suspend fun `when`(name: String, test: suspend WhenContext.() -> Unit) = add("When: $name", test)
private suspend fun add(name: String, test: suspend WhenContext.() -> Unit) =
context.registerTestCase(name, this@AbstractBehaviorSpec, { this@AbstractBehaviorSpec.WhenContext(this).test() }, this@AbstractBehaviorSpec.defaultTestCaseConfig, TestType.Container)
suspend fun And(name: String, test: suspend GivenAndContext.() -> Unit) = addAndContext(name, test)
suspend fun and(name: String, test: suspend GivenAndContext.() -> Unit) = addAndContext(name, test)

private suspend fun addAndContext(name: String, test: suspend GivenAndContext.() -> Unit) {
context.registerTestCase("And: $name", thisSpec, { thisSpec.GivenAndContext(this).test() }, thisSpec.defaultTestCaseConfig, TestType.Container)
}

suspend fun When(name: String, test: suspend WhenContext.() -> Unit) = addWhenContext(name, test)
suspend fun `when`(name: String, test: suspend WhenContext.() -> Unit) = addWhenContext(name, test)

private suspend fun addWhenContext(name: String, test: suspend WhenContext.() -> Unit) {
context.registerTestCase("When: $name", thisSpec, { thisSpec.WhenContext(this).test() }, thisSpec.defaultTestCaseConfig, TestType.Container)
}

suspend fun Then(name: String, test: suspend ThenContext.() -> Unit) = addThenContext(name, test)
suspend fun then(name: String, test: suspend ThenContext.() -> Unit) = addThenContext(name, test)

private suspend fun addThenContext(name: String, test: suspend ThenContext.() -> Unit) {
context.registerTestCase("Then: $name", thisSpec, { thisSpec.ThenContext(this).test() }, thisSpec.defaultTestCaseConfig, TestType.Container)
}

fun then(name: String) = TestScope(name, context)
}

@KotlinTestDsl
inner class GivenAndContext(val context: TestContext) {
suspend fun And(name: String, test: suspend GivenAndContext.() -> Unit) = addAndContext(name, test)
suspend fun and(name: String, test: suspend GivenAndContext.() -> Unit) = addAndContext(name, test)

private suspend fun addAndContext(name: String, test: suspend GivenAndContext.() -> Unit) {
context.registerTestCase("And: $name", thisSpec, { thisSpec.GivenAndContext(this).test() }, thisSpec.defaultTestCaseConfig, TestType.Container)
}

suspend fun When(name: String, test: suspend WhenContext.() -> Unit) = addWhenContext(name, test)
suspend fun `when`(name: String, test: suspend WhenContext.() -> Unit) = addWhenContext(name, test)

private suspend fun addWhenContext(name: String, test: suspend WhenContext.() -> Unit) {
context.registerTestCase("When: $name", thisSpec, { thisSpec.WhenContext(this).test() }, thisSpec.defaultTestCaseConfig, TestType.Container)
}

suspend fun Then(name: String, test: suspend ThenContext.() -> Unit) = addThenContext(name, test)
suspend fun then(name: String, test: suspend ThenContext.() -> Unit) = addThenContext(name, test)

private suspend fun addThenContext(name: String, test: suspend ThenContext.() -> Unit) {
context.registerTestCase("Then: $name", thisSpec, { thisSpec.ThenContext(this).test() }, thisSpec.defaultTestCaseConfig, TestType.Container)
}

fun then(name: String) = TestScope(name, context)
}

@KotlinTestDsl
inner class WhenContext(val context: TestContext) {
suspend fun Then(name: String, test: suspend TestContext.() -> Unit) = then(name, test)
suspend fun then(name: String, test: suspend TestContext.() -> Unit) =
context.registerTestCase("Then: $name", this@AbstractBehaviorSpec, test, this@AbstractBehaviorSpec.defaultTestCaseConfig, TestType.Test)
suspend fun And(name: String, test: suspend WhenAndContext.() -> Unit) = addAndContext(name, test)
suspend fun and(name: String, test: suspend WhenAndContext.() -> Unit) = addAndContext(name, test)

private suspend fun addAndContext(name: String, test: suspend WhenAndContext.() -> Unit) {
context.registerTestCase("And: $name", thisSpec, { thisSpec.WhenAndContext(this).test() }, thisSpec.defaultTestCaseConfig, TestType.Container)
}

suspend fun Then(name: String, test: suspend ThenContext.() -> Unit) = addThenContext(name, test)
suspend fun then(name: String, test: suspend ThenContext.() -> Unit) = addThenContext(name, test)

fun then(name: String): ThenScope = ThenScope(name, context)
private suspend fun addThenContext(name: String, test: suspend ThenContext.() -> Unit) {
context.registerTestCase("Then: $name", thisSpec, { thisSpec.ThenContext(this).test() }, thisSpec.defaultTestCaseConfig, TestType.Test)
}

fun then(name: String) = TestScope(name, context)
}

@KotlinTestDsl
inner class WhenAndContext(val context: TestContext) {
suspend fun And(name: String, test: suspend WhenAndContext.() -> Unit) = addAndContext(name, test)
suspend fun and(name: String, test: suspend WhenAndContext.() -> Unit) = addAndContext(name, test)

private suspend fun addAndContext(name: String, test: suspend WhenAndContext.() -> Unit) {
context.registerTestCase("And: $name", thisSpec, { thisSpec.WhenAndContext(this).test() }, thisSpec.defaultTestCaseConfig, TestType.Container)
}

suspend fun Then(name: String, test: suspend ThenContext.() -> Unit) = addThenContext(name, test)
suspend fun then(name: String, test: suspend ThenContext.() -> Unit) = addThenContext(name, test)

private suspend fun addThenContext(name: String, test: suspend ThenContext.() -> Unit) {
context.registerTestCase("Then: $name", thisSpec, { thisSpec.ThenContext(this).test() }, thisSpec.defaultTestCaseConfig, TestType.Test)
}

fun then(name: String) = TestScope(name, context)
}

@KotlinTestDsl
inner class ThenScope(val name: String, val context: TestContext) {
inner class ThenContext(val context: TestContext) {
suspend fun And(name: String, test: suspend ThenAndContext.() -> Unit) = addAndContext(name, test)
suspend fun and(name: String, test: suspend ThenAndContext.() -> Unit) = addAndContext(name, test)

private suspend fun addAndContext(name: String, test: suspend ThenAndContext.() -> Unit) {
context.registerTestCase("And: $name", thisSpec, { thisSpec.ThenAndContext(this).test() }, thisSpec.defaultTestCaseConfig, TestType.Test)
}

}

@KotlinTestDsl
inner class ThenAndContext(val context: TestContext) {
suspend fun And(name: String, test: suspend ThenAndContext.() -> Unit) = addAndContext(name, test)
suspend fun and(name: String, test: suspend ThenAndContext.() -> Unit) = addAndContext(name, test)

private suspend fun addAndContext(name: String, test: suspend ThenAndContext.() -> Unit) {
context.registerTestCase("And: $name", thisSpec, { thisSpec.ThenAndContext(this).test() }, thisSpec.defaultTestCaseConfig, TestType.Test)
}

suspend fun Then(name: String, test: suspend ThenContext.() -> Unit) = addThenContext(name, test)
suspend fun then(name: String, test: suspend ThenContext.() -> Unit) = addThenContext(name, test)

private suspend fun addThenContext(name: String, test: suspend ThenContext.() -> Unit) {
context.registerTestCase("Then: $name", thisSpec, { thisSpec.ThenContext(this).test() }, thisSpec.defaultTestCaseConfig, TestType.Test)
}

fun then(name: String) = TestScope(name, context)
}

@KotlinTestDsl
inner class TestScope(val name: String, val context: TestContext) {
suspend fun config(
invocations: Int? = null,
enabled: Boolean? = null,
timeout: Duration? = null,
threads: Int? = null,
tags: Set<Tag>? = null,
extensions: List<TestCaseExtension>? = null,
test: TestContext.() -> Unit) {
invocations: Int? = null,
enabled: Boolean? = null,
timeout: Duration? = null,
threads: Int? = null,
tags: Set<Tag>? = null,
extensions: List<TestCaseExtension>? = null,
test: TestContext.() -> Unit) {
val config = TestCaseConfig(
enabled ?: this@AbstractBehaviorSpec.defaultTestCaseConfig.enabled,
invocations ?: this@AbstractBehaviorSpec.defaultTestCaseConfig.invocations,
timeout ?: this@AbstractBehaviorSpec.defaultTestCaseConfig.timeout,
threads ?: this@AbstractBehaviorSpec.defaultTestCaseConfig.threads,
tags ?: this@AbstractBehaviorSpec.defaultTestCaseConfig.tags,
extensions ?: this@AbstractBehaviorSpec.defaultTestCaseConfig.extensions)
enabled ?: this@AbstractBehaviorSpec.defaultTestCaseConfig.enabled,
invocations ?: this@AbstractBehaviorSpec.defaultTestCaseConfig.invocations,
timeout ?: this@AbstractBehaviorSpec.defaultTestCaseConfig.timeout,
threads ?: this@AbstractBehaviorSpec.defaultTestCaseConfig.threads,
tags ?: this@AbstractBehaviorSpec.defaultTestCaseConfig.tags,
extensions ?: this@AbstractBehaviorSpec.defaultTestCaseConfig.extensions)

context.registerTestCase(name, this@AbstractBehaviorSpec, { test.invoke(this) }, config, TestType.Test)
}
}

fun Given(name: String, test: suspend GivenContext.() -> Unit) = given(name, test)
fun given(name: String, test: suspend GivenContext.() -> Unit) =
addTestCase("Given: $name", { this@AbstractBehaviorSpec.GivenContext(this).test() }, defaultTestCaseConfig, TestType.Container)
private val thisSpec: AbstractBehaviorSpec
get() = this@AbstractBehaviorSpec

}
@@ -1,12 +1,89 @@
package com.sksamuel.kotlintest.specs.behavior

import io.kotlintest.Spec
import io.kotlintest.TestCase
import io.kotlintest.TestResult
import io.kotlintest.matchers.numerics.shouldBeLessThan
import io.kotlintest.matchers.numerics.shouldNotBeLessThan
import io.kotlintest.matchers.string.shouldStartWith
import io.kotlintest.shouldBe
import io.kotlintest.specs.BehaviorSpec
import java.util.concurrent.atomic.AtomicInteger

class BehaviorSpecTest : BehaviorSpec() {

private val counter = AtomicInteger(0)

init {
Given("The string foo") {
val foo = "foo"

And("The string jar") {
val jar = "jar"

When("I add them together") {
val together = foo + jar

Then("It should be foojar") {
together shouldBe "foojar"
}
}
}

And("The string bar") {
val bar = "bar"

And("The string fuz") {
val fuz = "fuz"

When("I add them together") {
val together = foo + bar + fuz

And("Count their length") {
val length = together.length

And("Sum one to it") {
val sum = length + 1

Then("It should be 10") {
sum shouldBe 10
}
}

Then("It should be 9") {
length shouldBe 9

And("It should not be < 0") {
length shouldNotBeLessThan 0
}
}
}

Then("It should be foobarfuz") {
together shouldBe "foobarfuz"
}

And("I make it uppercase") {
val upper = together.toUpperCase()

Then("It should be FOOBARFUZ") {
upper shouldBe "FOOBARFUZ"
}

Then("It should start with F") {
upper shouldStartWith "F"
}
}
}
}
}

Then("It should be foo") {
foo shouldBe "foo"
}
}


given("a") {
`when`("b") {
then("c") {
Expand All @@ -19,5 +96,45 @@ class BehaviorSpecTest : BehaviorSpec() {
counter.get() shouldBe 3
}
}

given("A") {

counter.incrementAndGet()

and("b") {

counter.incrementAndGet()

and("c") {

counter.incrementAndGet()

`when`("d") {

counter.incrementAndGet()

and("e") {

counter.incrementAndGet()

then("f") {

counter.incrementAndGet()

and("g") {

counter.incrementAndGet()

}
}
}
}
}
}
}
}

override fun afterSpecClass(spec: Spec, results: Map<TestCase, TestResult>) {
counter.get() shouldBe 7
}
}

0 comments on commit 32afbe5

Please sign in to comment.