Friday, 14 October 2022

Kotlin / JUnit5: making a test conditional based on a custom condition

G'day:

I sat down to continue my "next" blog article, which will be about delegated properties in Kotlin (will cross link here once it's done), and as a first step I ran my tests to remind myself where I got to.

To my dismay I saw a bunch of failing tests. This in itself is OK as I am half-way through doing something, but the tsts that were failing were the ones for a different article: "Kotlin / TDD: writing the tests for a small web service". I slapped my forehead on this because obviously they would fail because I don't have that web service running ATM. And that's legit, so I needed to be able to conditionalise (is that a word? Is now I guess) those tests based on whether the web service is up.

I have previously looked at conditionalising tests in both Kotest ("Kotlin / Gradle / Kotest random exploration"), and then JUnit ("Kotlin: looking at JUnit instead of Kotest"), but in the case of the JUnit ones, they were all "canned" conditions, eg: @EnabledOnOs(OS.LINUX). In this case, I need to base the conditionality on my own logic.

Some quick googling landed me on the ever-helpful Baeldung site, with JUnit 5 Conditional Test Execution with Annotations › 6.2. Conditions. That said, I didn't find that article very helpful, and their examples were writing JS (yes: JS in a Java test; using Nashorn) in a string which seemed like a shit way of doing things to me, and I "knew" there must be a better way. I looked directly at the JUnit5 docs and found what I wanted: JUnit 5 User Guide › 2.8.6. Custom Conditions, their example was this:

@Test
@EnabledIf("customCondition")
void enabled() {
    // ...
}

@Test
@DisabledIf("customCondition")
void disabled() {
    // ...
}

boolean customCondition() {
    return true;
}

I can work with that. I set about writing an isUp function to call on my tests. I did TDD the behaviour of Ktor's HttpClient when getting no response from the server, along these lines:

@Test
fun `it throws an exception if the web service can't be reached`() {
    shouldThrow<ConnectException> {
        runBlocking {
            HttpClient().use { client ->
                client.get(webServiceBaseUrl)
            }
        }
    }.message shouldStartWith "Connection refused"
}

But this did not make the cut for my final code as it was a bit tricky to test the function that I use to conditionally enable the tests based on its own response (if you see what I mean: it's a bit self-referential). So only partial TDD on this I guess. Oops.

The final version of the isUp function was this:

fun isUp(): Boolean {
    return try {
        runBlocking {
            HttpClient().use { client ->
                client.head(webServiceBaseUrl)
            }
        }
        true
    } catch (e: ConnectException) {
        false
    }
}

I still love how a try/catch is an expression in Kotlin, so I can return it.

I had to horse around a bit when it came to actually using it in the annotation though. Unlike the examples in the docs, I did not want to make just a test method conditional, I wanted the whole class to be conditional, eg:

@DisplayName("Tests of WebService class")
@EnabledIf("isUp")
internal class WebServiceTest {

That's fine. But I had the function inside WebServiceTest, and the annotation could not find it:

Failed to evaluate condition [org.junit.jupiter.api.condition.EnabledIfCondition]: Cannot invoke non-static method [public final boolean junit.practical.WebServiceTest.isUp()] on a null target.

I don't really know what "on a null target" means here, but clearly I could not use a object method here. I thought it might be OK if I hauled the function out of the class and had it at the top level, but then it lost access to webServiceBaseUrl and I didn't want to repeat the URL in two places, so… noticed the error said non-static method, and so decided to sling the thing into a companion object (see "Kotlin: there's no such thing as static, apparently") of WebServiceTest:

@EnabledIf("isUp")
internal class WebServiceTest {

    private val webserviceUrl = webServiceBaseUrl + "numbers/"

    @Serializable
    data class SerializableNumber(val id: Int, val en: String, val mi: String)

    companion object {
        const val webServiceBaseUrl = "http://localhost:8080/"
        @JvmStatic
        fun isUp(): Boolean {
            return try {
                runBlocking {
                    HttpClient().use { client ->
                        client.head(webServiceBaseUrl)
                    }
                }
                true
            } catch (e: ConnectException) {
                false
            }
        }
    }
	// …
}    

And that worked fine. Note the @JvmStatic there. Just having the companion object works fine for Kotlin code, but I guess something to do with how the JUnit code is run requires that annotation to make the method seem static from Java's perspective too.

When I have the web service running:

And when it's off:

Compared to before:

Excellent.

Time for a break, and then I'll get back to that delegated properties article. The code for this is on GitHub @ WebServiceTest.kt.

Righto.

--
Adam