spring-bootkotlinkotest

Subclassing a kotest class and visibility to superclass members


I use kotest for SpringBoot integration tests. Given that I want to reuse some infrastructure between many tests, I wanted to make abstraction of the common fields and add helper functions that can be invoked in subclass tests. For example:

abstract class AbstractApiIntegrationTest(body: FreeSpec.() -> Unit) : FreeSpec(body) {

    @Value("\${api.http.endpoint}")
    private lateinit var apiEndpoint: String

     fun apiCall(block: RequestSpecification.() -> String): Response {
        return Given {
            cookie(authCookie())
            contentType(ContentType.JSON)

            ...

            body(this.block())
        } When {
            post(apiEndpoint)
        }
    }
})

Now, I want to reuse this helper function in the tests in subclasses:

class AlertUpdateTest(
    private val dataStorageHelper: DataStorageHelper,
    private val testDataHelper: TestDataHelper,
) : AbstractApiIntegrationTest({
    
    "Should pass" {
       
        apiCall {              <<<<------- issue is on this line
            getRequestBody()
        } Then {
            statusCode(OK.value())
        }
    }
})

However, apiCall is not visible as it was defined in the scope of the class (a class member), but the test has a scope of FreeSpec. I can use a companion object but that one does not have access to the class fields obviously (where I inject the value from SpringBoot).

What would be the best way to accomplish this? I know it's not an issue of Kotest per se but of Kotlin design. But Kotest is the one "forcing" the Spec style design of tests.


Solution

  • The problem is that you are using the "official" Kotest style, passing your test cases to the constructor of FreeSpec (or your subclass in your case). That constructor parameter takes only a function type that takes a FreeSpec receiver, which means you can't pass a lambda that takes your subclass as receiver.

    One workaround is to use an init block instead of a constructor parameter. I suspect this was, in fact, the main way to do in early, outdated versions of Kotest, as the animated image on the Framework page shows and the Testing Styles page says: "Then inside an init { } block, create your test cases." So, go ahead and do this:

    class AlertUpdateTest(
        private val dataStorageHelper: DataStorageHelper,
        private val testDataHelper: TestDataHelper,
    ) : AbstractApiIntegrationTest() {
    
        init {
           "Should pass" {
           
                apiCall {
                    getRequestBody()
                } Then {
                    statusCode(OK.value())
                }
            }
        }
    }
    

    I assume you've already sorted out how to get Spring Boot Test to inject the value for your apiEndpoint.