Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/composespecs2 #1144

Merged
merged 27 commits into from Jan 12, 2020
Merged

Feature/composespecs2 #1144

merged 27 commits into from Jan 12, 2020

Conversation

@sksamuel
Copy link
Member

sksamuel commented Jan 7, 2020

A design for #598 and #399

Note: This PR replaces the previous PR. Most notable change is that test factories (previously value specs) are not in themselves considered executable units.

Goal:

To allow specs to be composed of smaller reusable specs. These abstractions should be easily parameterized, dynamic and mappable.

Status:

Currently this PR is incomplete. The basics are working but more changes are required before this can be considered completed. I would rather merge sooner than later so that other contributors are not working against a codebase that could change.

Implementation:

This implementation adds a new type of "test container" called TestFactory. A TestFactory is a way of defining tests (and test related configuration) and then import those tests into a spec.

To create a test factory, we use a test factory builder function, for example:

val funTests = funSpec {
   test("foo") {
      1 + 1 shouldBe 2
   }
   test("bar") {
      "a" + "b" shouldBe "ab"
   }
}

Callbacks such as before/after can be defined inline, along with configuration. For example:

val funTests = funSpec {

   beforeTest {
      println("Starting test case ${it.name}")
   }

   afterTest { testCase, result ->
      println(testCase.name + " is completed with result " + result)
   }

   assertionMode = AssertionMode.Error

   test("my test") {
      1 + 1 shouldBe 2
   }
}

The aim of these test factories is to allow reusable composable specs. Since the result of a test factory builder function is just a TestFactory instance, they can be passed around, filtered, mapped, copied or whatever else you want to do. It's just a value. More importantly however, it can be included as part other specs.

val tests = stringSpec {
  "test 1" {
    "abc".shouldHaveLength(3)
  }
}

class MySpec : StringSpec() {
  init {
    include(tests)
    "test 2" {
      "".shouldHaveLength(0)
    }
  }
}

Note that you can even mix and match the spec styles when composing. The output reports will reflect the correct structure.

val funtests = funSpec {
  test("test 1") {
    "abc".shouldHaveLength(3)
  }
}

val stringtests = stringSpec {
  "test 2" {
    "zyx".shouldHaveLength(3)
  }
}

class MySpec : FunSpec() {
  init {
    include(funtests)
    include(stringtests)
  }
}

Since the test factory functions are just that - functions - they can dynamically generate tests based on parameters.

// defines an interface for the Semigroup typeclass
interface Semigroup<T> {
  fun combine(a: T, b: T): T
}

object StringSemigroup : Semigroup<String> {
  fun combine(a: String, b: String): T = "$a$b"
}

object LongSemigroup : Semigroup<Long> {
  fun combine(a: Long, b: Long): T = a + b
}

// every semigroup should uphold the law that ((ab)c) == (a(bc))
fun <T> associativity(sg: SemiGroup<T>, a: T, b: T, c: T) = funSpec {
  test("semigroup should be associative") {
    sg.combine(a, sg.combine(b, c)) shouldBe sg.combine(sg.combine(a, b), c)
  }
}

class StringSemigroupTest : FunSpec() {
  init {

    include(associativity(StringSemigroup, "a", "b", "c"))

    test("combination") {
        StringSemigroup.combine("a" ,"b") shouldBe "ab"
    }
  }
}

class LongSemigroupTest : FunSpec() {
  init {

    include(associativity(LongSemigroup, 1, 2, 3))

    test("combination") {
        LongSemigroup.combine(1, 2) shouldBe 3
    }
  }
}

Specs cannot be included into other specs, only test factories can be added to specs.

If you have some test factories and want to execute those without defining new tests, then you can use the CompositeSpec class.

val tests1 = funSpec { }
val tests2 = stringSpec { }
class SomeTests : CompositeSpec(tests1, tests2)
sksamuel added 7 commits Jan 6, 2020
@sksamuel sksamuel requested review from EarthCitizen and Kerooker Jan 7, 2020
sksamuel added 20 commits Jan 8, 2020
@sksamuel sksamuel merged commit f7f8b0e into master Jan 12, 2020
3 of 4 checks passed
3 of 4 checks passed
test test
Details
continuous-integration/appveyor/pr AppVeyor build succeeded
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details
@sksamuel sksamuel deleted the feature/composespecs2 branch Jan 12, 2020
ashishkujoy added a commit to ashishkujoy/kotlintest that referenced this pull request Jan 12, 2020
* Reusable composable specs

* Reusable composable specs

* Reusable composable specs

* Reusable composable specs

* Reusable composable specs

* Reusable composable specs

* Added readme

* Added readme

* composable specs

* Reusable composable specs

* Reusable composable specs

* composable specs

* composable specs

* Reusable composable specs

* dep upgrades

* dep upgrades

* Some fp updates

* Some fp updates

* Reusable composable specs

* Reusable composable specs

* Reusable composable specs

* Reusable composable specs

* Reusable composable specs

* Reusable composable specs

* Reusable composable specs

* Reusable composable specs

* Reusable composable specs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
1 participant
You can’t perform that action at this time.