Kotest v3.1.0 Release Notes

    • Simplified Setup

    ๐Ÿ‘• In KotlinTest 3.1.x it is sufficent to enable JUnit in the test block of your gradle build instead of using the gradle junit plugin. This step is the same as for any test framework that uses the JUnit Platform.

    โœ… Assuming you have gradle 4.6 or above, then setup your test block like this:

    test {
        useJUnitPlatform()
    }
    

    โœ… You can additionally enable extra test logging:

    test {
        useJUnitPlatform()
        testLogging {
            events "PASSED", "FAILED", "SKIPPED", "STANDARD_OUT", "STANDARD_ERROR"
        }
    }
    
    • โœ… Instance Per Test for all Specs

    ๐Ÿ’… In the 3.0.x train, the ability to allow an instance per test was removed from some spec styles due to ๐Ÿ’… implementation difficulties. This has been addressed in 3.1.x and so all spec styles now allow instance ๐Ÿš€ per test as in the 2.0.x releases. Note: The default value is false, so tests will use a single shared โœ… instance of the spec for all tests unless the isInstancePerTest() function is overriden to return true.

    • ๐Ÿ’ฅ Breaking Change: Config Syntax

    โœ… The syntax for config has now changed. Instead of a function call after the test has been defined, it is โœ… now specified after the name of the test.

    So, instead of:

    "this is a test" {
    }.config(...)
    

    You would now do:

    "this is a test".config(...) {
    }
    
    • Matchers as extension functions

    All matchers can now be used as extension functions. So instead of:

    file should exist()
    
    or
    
    listOf(1, 2) should containNull()
    

    You can do:

    file.shouldExist()
    
    or
    
    listOf(1, 2).shouldContainNull()
    

    Note: The infix style is not deprecated and will be supported in future releases, but the extension function ๐Ÿ’… is intended to be the preferred style moving forward as it allows discovery in the IDE.

    • Dozens of new Matchers

    even and odd

    โœ… Tests that an Int is even or odd:

    4 should beEven()
    3 shouldNot beEven()
    
    3 should beOdd()
    4 shouldNot beOdd()
    

    beInRange

    Asserts that an int or long is in the given range:

    3 should beInRange(1..10)
    4 should beInRange(1..3)
    

    haveElementAt

    Checks that a collection contains the given element at a specified index:

    listOf("a", "b", "c") should haveElementAt(1, "b")
    listOf("a", "b", "c") shouldNot haveElementAt(1, "c")
    

    Help out the type inferrer when using nulls:

    listOf("a", "b", null) should haveElementAt<String?>(2, null)
    

    readable, writeable, executable and hidden

    โœ… Tests if a file is readable, writeable, or hidden:

    file should beRadable()
    file should beWriteable()
    file should beExecutable()
    file should beHidden()
    

    absolute and relative

    โœ… Tests if a file's path is relative or absolute.

    File("/usr/home/sam") should beAbsolute()
    File("spark/bin") should beRelative()
    

    startWithPath(path)

    โœ… Tests if a file's path begins with the specified prefix:

    File("/usr/home/sam") should startWithPath("/usr/home")
    File("/usr/home/sam") shouldNot startWithPath("/var")
    

    haveSameHashCodeAs(other)

    Asserts that two objects have the same hash code.

    obj1 should haveSameHashCodeAs(obj2)
    "hello" shouldNot haveSameHashCodeAs("world")
    

    haveSameLengthAs(other)

    Asserts that two strings have the same length.

    "hello" should haveSameLengthAs("world")
    "hello" shouldNot haveSameLengthAs("you")
    

    haveScheme, havePort, haveHost, haveParameter, havePath, haveFragment

    Matchers for URIs:

    val uri = URI.create("https://localhost:443/index.html?q=findme#results")
    uri should haveScheme("https")
    uri should haveHost("localhost")
    uri should havePort(443)
    uri should havePath("/index.html")
    uri should haveParameter("q")
    uri should haveFragment("results")
    
    • Date matchers - before / after / haveSameYear / haveSameDay / haveSameMonth / within
    • Collections - containNull, containDuplicates
    • Futures - completed, cancelled
    • String - haveLineCount, contain(regex)
    • Types - haveAnnotation(class)

    • Arrow matcher module

    A new module has been added which includes matchers for Arrow - the popular and awesome functional programming library for Kotlin. To include this module add kotlintest-assertions-arrow to your build.

    The included matchers are:

    Option - Test that an Option has the given value or is a None. For example:

    val option = Option.pure("foo")
    option should beSome("foo")
    
    val none = None
    none should beNone()
    

    Either- Test that an Either is either a Right or Left. For example:

    Either.right("boo") should beRight("boo")
    Either.left("boo") should beLeft("boo")
    

    NonEmptyList- A collection (no pun intended) of matchers for Arrow's NonEmptyList. These mostly mirror the equivalent Collection matchers but for NELs. For example:

    NonEmptyList.of(1, 2, null).shouldContainNull()
    NonEmptyList.of(1, 2, 3, 4).shouldBeSorted<Int>()
    NonEmptyList.of(1, 2, 3, 3).shouldHaveDuplicates()
    NonEmptyList.of(1).shouldBeSingleElement(1)
    NonEmptyList.of(1, 2, 3).shouldContain(2)
    NonEmptyList.of(1, 2, 3).shouldHaveSize(3)
    NonEmptyList.of(1, 2, 3).shouldContainNoNulls()
    NonEmptyList.of(null, null, null).shouldContainOnlyNulls()
    NonEmptyList.of(1, 2, 3, 4, 5).shouldContainAll(3, 2, 1)
    

    Try - Test that a Try is either Success or Failure.

    Try.Success("foo") should beSuccess("foo")
    Try.Failure<Nothing>(RuntimeException()) should beFailure()
    

    Validation - Asserts that a Validation is either Valid or an Invalid

    Valid("foo") should beValid()
    Invalid(RuntimeException()) should beInvalid()
    
    • Generator Bind

    A powerful way of generating random class instances from primitive generators is to use the new bind function. A simple example is to take a data class of two fields, and then use two base generators and bind them to create random values of that class.

    data class User(val email: String, val id: Int)
    
    val userGen = Gen.bind(Gen.string(), Gen.positiveIntegers(), ::User)
    
    assertAll(userGen) {
      it.email shouldNotBe null
      it.id should beGreaterThan(0)
    }
    
    • โœ… Property Testing: Classify

    ๐Ÿ‘€ When using property testing, it can be useful to see the distribution of values generated, to ensure you're getting a good spread of values and not just trival ones. For example, you might want to run a test on a String and you want to ensure you're getting good amounts of strings with whitespace.

    To generate stats on the distribution, use classify with a predicate, a label if the predicate passes, and a label if the predicate fails. For example:

    assertAll(Gen.string()) { a ->
        classify(a.contains(" "), "has whitespace", "no whitespace")
        // some test
    }
    

    And this will output something like:

    63.70% no whitespace
    36.30% has whitespace
    

    ๐Ÿ‘€ So we can see we're getting a good spread of both types of value.

    You don't have to include two labels if you just wish to tag the "true" case, and you can include more than one classification. For example:

    forAll(Gen.int()) { a ->
        classify(a == 0, "zero")
        classify(a % 2 == 0, "even number", "odd number")
        a + a == 2 * a
    }
    

    This will output something like:

    51.60% even number
    48.40% odd number
    0.10% zero
    
    • โœ… Property Testing: Shrinking

    • ๐Ÿท Tag Extensions

    A new type of extension has been added called TagExtension. Implementations can override the tags() function defined in this interface to dynamically return the Tag instances that should be active at any moment. The existing ๐Ÿ‘• system properties kotlintest.tags.include and kotlintest.tags.exclude are still valid and are not deprecated, but โž• adding this new extension means extended scope for more complicated logic at runtime.

    โœ… An example might be to disable any Hadoop tests when not running in an environment that doesn't have the hadoop home env variable set. After creating a TagExtension it must be registered with the project config.

    object Hadoop : Tag()
    
    object HadoopTagExtension : TagExtension {
      override fun tags(): Tags =
          if (System.getenv().containsKey("HADOOP_HOME")) Tags.include(Hadoop) else Tags.exclude(Hadoop)
    }
    
    object MyProjectConfig : AbstractProjectConfig() {
      override fun extensions(): List<Extension> = listOf(HadoopTagExtension)
    }
    
    object SimpleTest : StringSpec({
      "simple test" {
        // this test would only run on environments that have hadoop configured
      }.config(tags = setOf(Hadoop))
    })
    
    • Discovery Extensions: instantiate()

    Inside the DiscoveryExtension interface the function fun <T : Spec> instantiate(clazz: KClass<T>): Spec? has been added which ๐Ÿ‘ allows you to extend the way new instances of Spec are created. By default, a no-args constructor is assumed. However, if this ๐Ÿ‘ function is overridden then it's possible to support Spec classes which have other constructors. For example, the Spring module ๐Ÿ‘ now supports constructor injection using this extension. Other use cases might be when you want to always inject some config class, โœ… or if you want to ensure that all your tests extend some custom interface or superclass.

    As a reminder, DiscoveryExtension instances are added to Project config.

    • System out / error extensions

    โœ… An extension that allows you to test for a function that writes to System.out or System.err. To use this extension add the module kotlintest-extensions-system to your build.

    By adding the NoSystemOutListener or NoSystemErrListener to your config or spec classes, anytime a function tries to write to either of these streams, a SystemOutWriteException or SystemErrWriteException will be raised with the string that the function tried to write. This allows you to test for the exception in your code.

    For example:

    class NoSystemOutOrErrTest : StringSpec() {
    
      override fun listeners() = listOf(NoSystemOutListener, NoSystemErrListener)
    
      init {
    
        "System.out should throw an exception when the listener is added" {
          shouldThrow<SystemOutWriteException> {
            System.out.println("boom")
          }.str shouldBe "boom"
        }
    
        "System.err should throw an exception when the listener is added" {
          shouldThrow<SystemErrWriteException> {
            System.err.println("boom")
          }.str shouldBe "boom"
        }
      }
    }
    
    • System.exit extension

    ๐Ÿ‘• Another extension that is part of the kotlintest-extensions-system module. This extension will allow you to test if System.exit(Int) is invoked in a function. It achieves this by intercepting any calls to System.exit and instead of terminating the JVM, it will throw a SystemExitException with the exit code.

    For example:

    class SystemExitTest : StringSpec() {
    
      override fun listeners() = listOf(SpecSystemExitListener)
    
      init {
    
        "System.exit should throw an exception when the listener is added" {
          shouldThrow<SystemExitException> {
            System.exit(123)
          }.exitCode shouldBe 123
        }
      }
    }
    
    • โšก๏ธ Spring Module Updates

    โšก๏ธ The spring extension module kotlintest-extensions-spring has been updated to allow for constructor injection. This new extension is called SpringAutowireConstructorExtension and must be added to your `ProjectConfig. โœ… Then you can use injected dependencies directly in the primary constructor of your test class.

    For example:

    @ContextConfiguration(classes = [(Components::class)])
    class SpringAutowiredConstructorTest(service: UserService) : WordSpec({
      "SpringListener" should {
        "have autowired the service" {
          service.repository.findUser().name shouldBe "system_user"
        }
      }
    })
    
    • JUnit 4 Runner

    ๐Ÿ‘• A JUnit 4 runner has been added which allows KotlinTest to run using the legacy JUnit 4 platform. ๐Ÿ‘• To use this, add kotlintest-runner-junit4 to your build instead of kotlintest-runner-junit5.

    Note: This is intended for use when junit5 cannot be used. It should not be the first choice as functionality is restricted.

    Namely:

    • โœ… In intellij, test output will not be nested
    • ๐Ÿ‘ Project wide beforeAll/afterAll cannot be supported.