Showing posts with label Kotlin. Show all posts
Showing posts with label Kotlin. Show all posts

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

Sunday 9 October 2022

Kotlin / TDD: using JetBrains/Exposed to read from the DB

G'day:

Earlier today I wrote "Kotlin / TDD: writing the tests for a small web service". I focused on learning how to make HTTP requests and test the results thereof. The last bit I needed to do for the exercise I had at hand was to write a test to verify my POST request was actually writing to the DB. To do this I'd need to work out how to actually make a DB call in Kotlin, and I've not got that far yet. I decided to leave that to the next article, and come back to it later. Then I got to thinking: everything else has proven to be really easy so far: I bet this will be too. And it started to bug me so I decided "to hell with it: let's find out". So here I am again. Spoilers: it was easy, and this will be a short article.


It saves the new object to the database

Full disclosure: no it doesn't. I have not modified the test web service to do anything of the sort: it does not touch the DB, it just responds saying "yeah, done", and I already have the test data in there. So the data is "mocked" I guess. All I'm doing here is testing that I can fetch something from the DB.

This test is more complicated. Firstly I have to define the entity that represents the data:

class TranslatedNumber(id: EntityID<Int>) : IntEntity(id) {
    object TranslatedNumbers : IntIdTable("numbers") {
        val en: Column<String> = varchar("en", 50)
        val mi: Column<String> = varchar("mi", 50)
    }
    companion object : IntEntityClass<TranslatedNumber>(TranslatedNumbers)

    var en by TranslatedNumbers.en
    var mi by TranslatedNumbers.mi
}

I pretty much got all that from the docs: JetBrains/Exposed › Getting Started › Your first Exposed DAO, although I refactored things a bit.

There's two parts to this:

It's all pretty clear, I think?

Now the test:

@Test
@EnabledIfEnvironmentVariable(
    named = "MARIADB_PASSWORD",
    matches = ".*",
    disabledReason = "This test requires a MariaDB database, so it needs the password"
)
fun `It saves the new object to the database`() {
    val six = SerializableNumber(6, "six", "ono")
    runBlocking {
        HttpClient() {
            install(ContentNegotiation) {
                json()
            }
        }.use { client ->
            val response = client.post(webserviceUrl) {
                contentType(ContentType.Application.Json)
                setBody(six)
            }

            response.status shouldBe HttpStatusCode.Created

        }
        Database.connect(
            "jdbc:mysql://localhost:3308/db1",
            driver = "com.mysql.cj.jdbc.Driver",
            user = "user1",
            password = System.getenv("MARIADB_PASSWORD")
        )
        transaction {
            addLogger(StdOutSqlLogger)

            TranslatedNumber.findById(six.id).asClue {
                it shouldNotBe null
                it!!.en shouldBe six.en
                it.mi shouldBe six.mi
            }
        }
    }
}

I've greyed-out the stuff that's mostly the same as the previous tests from the earlier article. The interesting bits are:

  • I don't want to hard-code my DB password here (especially as it's going into source control), so I use an environment variable. And if the env variable ain't set: skip the test. I was quite let down that the reason message doesn't display in the test output.
  • If I add that logger there, all the SQL statements echo to std out. This makes debugging issues easier.

    SQL: SELECT numbers.id, numbers.en, numbers.mi FROM numbers WHERE numbers.id = 6
  • Apparently all Exposed DB calls need to occur within one of these transaction blocks. That's not me deciding to run a single select query in a transaction, it's a hard requirement of Exposed.
  • The !! operator throws an exception at runtime if the expression is null. I guess we only need the one of them as it's implied that it's an all/nothing thing if the object has been initialised. Copilot wrote that code [cough]. However if I take it out, I get an error:

    Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type TranslatedNumber?

    If I use ?. instead, then I need it on both en and mi.

One thing I am left wondering about here: there doesn't seem to be any direct coupling between the DB connection and the transaction block. What if I had multiple DB connections? I see there is a discussion on GitHub: Multiple databases #93, and I share the opinion that I am not in love with how they have resolved this: Transactions › Working with a multiple databases. Basically it's this:

val conn = Database.connect(
    // etc
)
transaction(conn) {
   // etc
}

I was expecting this sort of thing:

Database.connect(
    // etc
).transaction {
   // etc
}

// or

val conn = Database.connect(
    // etc
)

// ...

conn.transaction {
   // etc
}

That looks more Kotlin-idiomatic to me (he says, having been using Kotlin for like a coupla weeks… ;-).


OK that was easy. One last observation: "Exposed" is a shit name. It reads like a red-banner-tabloid headline, and… it's a verb (this might have been lost on the JetBrains crew, as I don't think they're native English speakers). It's also not hugely google-able without prefixing it with "kotlin". Ah well.

And now I am giving up for the day. That's enough Kotlin. Code is here: WebServiceTest.kt and TranslatedNumber.kt. This latest tag also includes some housekeeping changes, but those are the important bits.

Righto.

--
Adam

Kotlin / TDD: writing the tests for a small web service

G'day:

One of my tasks @ work is to check out how to test a web service. I started with this yesterday's article: Kotlin: getting the khttp library installed and running… then… getting rid of it and using something else, but that was justa "proof of concept" of making an HTTP call, and examining its results. Today I'm gonna write the actual tests we need for the interface for a web service. Note: I still don't know enough about Ktor to create a web service with it, so I'm gonna fall back to using CFML for that end of things. I'm not gonna bother with the code for that here; the tests will demonstrate whether or not the web service is fulfilling its contract. But I will be TDDing this. Currently I have zero tests and zero code to test. Let's get on with it.


It should return a 200-OK on the root URI on a valid request

fun `It should return a 200-OK on the root URI on a valid request`() {
    runBlocking {
        HttpClient().use { client ->
            val response = client.get(webserviceUrl)
            response.status shouldBe HttpStatusCode.OK
        }
    }
}

NB: I will only comment if there's something note-worthy or not obvious. All tests will initially fail until I implement the relevant bit of the web service, eg:

expected:<200 OK> but was:<404 Not Found>
Expected :200 OK
Actual   :404 Not Found

It should return a 406-NOT-ACCEPTABLE and suggest the expected type if the Accepts header is not application/json

@Test
fun `It should return a 406-NOT-ACCEPTABLE and suggest the expected type if the Accepts header is not application-json`() {
    runBlocking {
        HttpClient().use { client ->
            val response = client.get(webserviceUrl) {
                header("Accept", "text/plain")
            }

            response.status shouldBe HttpStatusCode.NotAcceptable
            response.body() as String shouldBe """["application/json"]"""
        }
    }
}

Note: It's a Kotlin/JVM limitation that I have to use application-json rather than application/json in the method name there. See Why Kotlin does not allow slash in identifiers, which in turn points the reader to Java Virtual Machine Specification › Chapter 4. The class File Format:

4.2.2. Unqualified Names

Names of methods, fields, local variables, and formal parameters are stored as unqualified names. An unqualified name must contain at least one Unicode code point and must not contain any of the ASCII characters . ; [ / (that is, period or semicolon or left square bracket or forward slash).

When I initially just referenced response.body() shouldBe """["application/json"]""" Kotlin was saying "Not enough information to infer type variable", and after some googling I landed on Type checks and casts › "Unsafe" cast operator, which explains that as String thing I have there.

Also note I needed to update the 200-OK test to pass the correct Accept header.


It returns an array of Numbers as a JSON array

This one was a bit trickier, but the docs were reasonably helpful, and I'm pleased with the outcome.

@Test
fun `It returns an array of Numbers as a JSON array`() {
    @Serializable
    data class Number(val id: Int, val en: String, val mi: String)

    runBlocking {
        HttpClient() {
            install(ContentNegotiation) {
                json()
            }
        }.use { client ->
            val response = client.get(webserviceUrl) {
                header("Accept", "application/json")
            }
            response.status shouldBe HttpStatusCode.OK
            response.body() as List<Number> shouldBe listOf(
                Number(1, "one", "tahi"),
                Number(2, "two", "rua"),
                Number(3, "three", "toru"),
                Number(4, "four", "wha")
            )
        }
    }
}

I had guessed that there'd be a way to deliver objects straight to the app from an HTTP request, and a quick google set me on the right path here, landing me on these docs: Content negotiation and serialization. What's super helpful in these docs as they link through to examples of everything (including the build.gradle.kt file, as I needed to add some dependencies - see further down), eg: ktor-documentation/codeSnippets/snippets/client-json-kotlinx/src/main/kotlin/com/example/Application.kt and ktor-documentation/codeSnippets/snippets/client-json-kotlinx/build.gradle.kts. I just followed those and changed the bits I needed to change.

Steps:

  • tell the client that I'm gonna expect it to work out what the content represents (boilerplate).
  • Create a data class that the client will use to deserialize the data as. NB: it needs to be tagged as being serializable, because well: that's what we're doing here.
  • Specify that type - or in this case a list of that type - as the body value.

Done.

I will admit than initially I thought I had messed-up because instead of getting a "nup, it's not a list of Numbers" in my failing test, I got this monstrousity:

io.ktor.client.call.NoTransformationFoundException: No transformation found: class io.ktor.utils.io.ByteBufferChannel -> class kotlin.collections.List

But it turns out that just means "um… yer request ain't returning JSON". Which it indeed was not. Once I got it to return something (anything) and the correct content-type, I got a more on-point error:

Expected start of the array '[', but had 'EOF' instead at path: $
JSON input: ""  

And from there I tweaked the web service to return a coupla wrong things to see how Ktor reported on deserialization failures, and it was all helpful.

All the examples I saw for this was only deserializing one object, so I was slightly cautious as to how to deal with a JSON array, but I took a punt on just specifying response.body() as List<Number>, thinking Ktor's probably clever enough to expect this sort of thing, and - lo - it did. Nice one.

As I mentioned above, I also had to tweak the dependencies and plugins a bit (build.gradle.kts):

plugins {
    kotlin("jvm") version "1.7.10"
    kotlin("plugin.serialization").version("1.7.10")
    application
}

// …

dependencies {
    // …
    implementation("io.ktor:ktor-client-core:$ktorVersion")
    implementation("io.ktor:ktor-client-cio:$ktorVersion")
    implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
    implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
    // …
}

It will accept a POST request of an object as JSON and return the same object as confirmation, and its URL

This was really straight-forward, having done all the hard-bit in the previous one:

@Test
fun `It will accept a POST request of an object as JSON and return the same object as confirmation, and its URL`() {
    val five = Number(5, "five", "rima")
    runBlocking {
        HttpClient() {
            install(ContentNegotiation) {
                json()
            }
        }.use { client ->
            val response = client.post(webserviceUrl) {
                contentType(ContentType.Application.Json)
                setBody(five)
            }
            response.status shouldBe HttpStatusCode.Created
            response.body() as Number shouldBe five
            response.headers["Location"] shouldBe "${webserviceUrl}5"
        }
    }
}

By now there's absolutely nothing "unexpected" in this, I think.


OK that's enough for a Sunday afternoon. This weekend I've managed to work out how to make HTTP requests in my tests, how to set/check headers, response codes and the body of the responses. I've posted an object and received objects back again, letting Ktor handle the (de)serialization.

These tests are only testing the interface of the web webservice, which is fine and an essential part of building a web service. However the next thing on the list is to do end to end tests: check the underlying data store that new objects are being created (cos they absolutely are not at the moment ;-)), and the correct data is being returned, etc. I have no idea how to make a DB call in Kotlin yet. Something for the coming week, I guess.

The code is on GitHub @ /src/test/kotlin/junit/practical/WebServiceTest.kt, and the web stub service I was creating to make the tests pass is this lot: adamcameron/Numbers.cfc.

Righto.

--
Adam

Saturday 8 October 2022

Kotlin/Gradle: abstracting versions into a config file, and wondering what delegated properties are

G'day:

Currently I have just been following the instructions fairly slavishly when it comes to my dependency management in my application. If some readme file says "put testImplementation("io.kotest:kotest-runner-junit5:5.5.0") in yer Gradle build file to include this dependency", then that's exactly what I do. Sometimes I monkey with the version if IntelliJ says there's a newer version. So I've ended up with this lot:

dependencies {
    testImplementation(kotlin("test"))
    testImplementation("io.kotest:kotest-runner-junit5:5.5.0")
    testImplementation("io.kotest:kotest-assertions-core:5.5.0")
    testImplementation("com.github.stefanbirkner:system-lambda:1.2.1")
    testImplementation("org.apache.commons:commons-lang3:3.12.0")
    testImplementation("org.junit.jupiter:junit-jupiter:5.9.0")
    testImplementation("org.junit.jupiter:junit-jupiter:5.9.0")
    testImplementation("io.kotest:kotest-framework-datatest:5.5.0")
    implementation("io.ktor:ktor-client-core:2.1.2")
    implementation("io.ktor:ktor-client-cio:2.1.2")
}

That's fine. However then I looked at the sample project that the Ktor Project Generator generates, and I see this in my its build file:

val ktor_version: String by project
val kotlin_version: String by project
val logback_version: String by project

// ...

dependencies {
    implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
    implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
    implementation("ch.qos.logback:logback-classic:$logback_version")
    testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}

What's all this by project carry-on? I looked for a string match for ktor_version, and found this lot in gradle.properties:

ktor_version=2.1.2
kotlin_version=1.7.20
logback_version=1.2.11
kotlin.code.style=official

(Mine currently only has that last line)

OK so that seems like a good idea, I'm gonna run with this approach for the dependencies in my scratch project too:

kotlin.code.style=official
kotest_version=5.5.0
kotlin_version=1.7.10
system_lambda_version=1.2.1
commons_lang_3_version=3.12.0
junit_jupiter_version=5.9.0
ktor_client_core_version=2.1.2
dependencies {
    testImplementation(kotlin("test"))
    testImplementation("io.kotest:kotest-runner-junit5:$kotest_version")
    testImplementation("io.kotest:kotest-assertions-core:$kotest_version")
    testImplementation("io.kotest:kotest-framework-datatest:$kotest_version")
    testImplementation("com.github.stefanbirkner:system-lambda:$system_lambda_version")
    testImplementation("org.apache.commons:commons-lang3:$commons_lang_3_version")
    testImplementation("org.junit.jupiter:junit-jupiter:$junit_jupiter_version")
    testImplementation("org.junit.jupiter:junit-jupiter:$junit_jupiter_version")
    implementation("io.ktor:ktor-client-core:$ktor_client_core_version")
    implementation("io.ktor:ktor-client-cio:$ktor_client_core_version")
}

When I rebuilt: it all went fine, and all my tests still pass.

I noted in the Ktor build file the Kotlin plug-in version was still a static inline string:

plugins {
    application
    kotlin("jvm") version "1.7.20"
    // …
}

I tried to make that dynamic as well:

plugins {
    kotlin("jvm") version kotlin_version
    application
}

But INtelliJ barfed at this saying:

'val kotlin_version: String' can't be called in this context by implicit receiver. Use the explicit one if necessary

I googled that, and landed on Why can’t I use val inside Plugins {}? which pointed me to this: Using Gradle Plugins › Limitations of the plugins DSL, and the important bit there is:

This requires that plugins be specified in a way that Gradle can easily and quickly extract, before executing the rest of the build script. It also requires that the definition of plugins to use be somewhat static.

(My emphasis).

Fair cop. I'll not try to do that then.

All this is lovely, but how does this work. What magic is "by project" doing? I guessed that by is some inline function, and project is its argument, similar to in a test where I have x shouldBe y. I can drill down on by, and it takes me to org.gradle.kotlin.dsl.ProjectExtensions:

operator fun Project.provideDelegate(any: Any?, property: KProperty<*>): PropertyDelegate =
    propertyDelegateFor(this, property)

I'm none-the-wiser looking at that, but it gave me something to google (googling on phrases where "by" is the important word was not a particularly edifying or fruitful endeavour). I found Delegated properties, the summary of which is:

With some common kinds of properties, even though you can implement them manually every time you need them, it is more helpful to implement them once, add them to a library, and reuse them later.

Kotlin supports delegated properties:

class Example {
    var p: String by Delegate()
}

OK, that answers that. There is also a bunch more interesting stuff in that article which I will need to look at later. Oh and what's the "project" part of "by project"? It's just an object that wraps up the values I set in gradle.properties. Slick.

I'm really glad to understand what's going on there, and also looking forward to looking at "delegated properties" some more. Later.


That was painless and short. I quite like that I have de-duped my versions there, and separated them from their usage. The changes made for this exercise are in GitHub @ kotlin_scratch, tag 1.12. Not that it's very intertesting code today, I s'pose ;-)

Righto.

--
Adam

Friday 7 October 2022

Kotlin: getting the khttp library installed and running... then... getting rid of it and using something else

G'day:

So 50% of this was a largely fruitless exercise in the end. Other than the fact that I learned some stuff that I think is worth knowing for future reference.

The next thing on my Kotlin/testing list is a more practical exercise: I need to be able to write tests for web service endpoints. To do that I need to be able to make HTTP requests. I second-guessed there'd be really awful boilerplate-laiden Java ways of doing things, but I wanted a Kotlin-idiomatic way of doing it.


khttp

I googled "kotlin http requests", and one of the first links was to an article on the Baeldung website: "HTTP Requests with Kotlin and khttp". Their articles ae usually pretty solid, so I didn't look any further. It was especially appealing because they made a point of saying:

On the JVM we have several available options, from lower-level to very high-level libraries[…]. However, most of them are targeted primarily at Java programs.

In this article, we’re going to look at khttp, an idiomatic Kotlin library for consuming HTTP-based resources and APIs.

Perfect.

First I had to install this khttp thing. The instructions were for a hand-cranked Maven install, but I can't be arsed with that at the moment, and I wanna use Gradle via my build.gradle.kts file. Other things I've looked at have supplied the installation instructions for that, which is really handy for a n00b like me as I don't really know what I'm doing and what the various values mean in the Maven XML. And TBH: I don't want to know. But I figured I could work out from the XML:

<dependency>
    <groupId>khttp</groupId>
    <artifactId>khttp</artifactId>
    <version>0.1.0</version>
</dependency>

<repository>
    <id>central</id>
    <url>http://jcenter.bintray.com</url>
</repository>

I need the repository cos it's not on Maven Central yet apparently. Cool: I know that one can specify repositories and dependencies in the Gradle file, so how hard could it be.

repositories {
    mavenCentral()
    maven("https://jcenter.bintray.com")
}

dependencies {
    // …
    testImplementation("khttp:khttp:0.1.0")
}

All I needed to do is to change the repo's scheme from http to https, because with http I was getting an error:

Using insecure protocols with repositories, without explicit opt-in, is unsupported.

And that's fair enough. It installed OK over https.

I created my test:

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

    val serviceUrl = "https://example.com/"
    @Test
    fun `it returns a 200`() {
        val response = khttp.get(serviceUrl)

        response.statusCode shouldBe 200
    }
}

And ran it:

Unable to make field private java.lang.String java.net.URL.host accessible: module java.base does not "opens java.net" to unnamed module @645aa696 java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String java.net.URL.host accessible: module java.base does not "opens java.net" to unnamed module @645aa696 at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)

Not what I wanted to see. Not least of all cos I have NFI what that means. I poss should have noticed the mention of reflection in which case I might have recalled that there's been warnings emiting from Java about this when I start-up Lucee:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by [x] to method [y]
WARNING: Please consider reporting this to the maintainers of [x]
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

Had I thought about it I would have landed on the fix sooner. It took me about an hour to find a comment on the khttp projects GitHub issues: ascclemens/khttp issue #88. I am running on a Java 16 JDK, and that reflective thing got blocked from that version on. The best answer on the issue was:

For enabling khttp 1.0 with JDK 17.0 I use these VM arguments:

--add-opens java.base/java.net=ALL.UNNAMED --add-opens java.base/sun.net.www.protocol.https=ALL-UNNAMED

Cool. I had a hunt around in the settings of IntelliJ a bit to find out where I could sling those. I found a few places that took VM arguments, which looked promising, but none worked when I rebuild the project. I also found a way of putting them in my build file:

application {
    mainClass.set("MainKt")
    listOf(
        "--add-opens java.base/java.net=ALL.UNNAMED",
        "--add-opens java.base/sun.net.www.protocol.https=ALL-UNNAMED"
    ).also { applicationDefaultJvmArgs = it }
}

Which is good to know, but also didn't work.

In the end I downloaded Java 15 JDK and used that instead. For future me, the setting that matter is the Gradle JVM:

It is not just a matter of changing the Java or Kotlin byte-code versions:

I could switch those to 15, and it didn't make a difference. But once I changed Gradles JVM to 15, it worked:

But do you know what? I wasn't so happy with reverting my JVM version to accommodate a library I was using for testing, so I decided to see if there was another one. And this is when I wished I had looked below the fold on the first page of Google results…


Ktor

Ktor is the framework we have decided to use for our project @ work, so it kinda makes sense to use its own tooling. I'm gonna have a look at their offering.

Installation was simple:

implementation("io.ktor:ktor-client-core:2.1.2")
implementation("io.ktor:ktor-client-cio:2.1.2")

The first one is self-explanatory; the second is the underlying engine the client uses it seems. There's some docs: Ktor Client / Developing applications / Setting up a client / Engines.

From there the test was a bit of horsing around:

fun `it returns a 200`() {
    runBlocking {
        HttpClient().use { client ->
            val response = client.get("https://jsonplaceholder.typicode.com/todos/1")
            response.status shouldBe HttpStatusCode.OK
        }
    }
}

The CIO engine is asynchronous, so all this is using coroutines which I don't have headspace for ATM, but it seems the "async await" approach to making async code run in non-async code is to sling a runBlocking block around it. Fine.

One cool Kotlin thing here is that I did have this code written like this:

fun `it returns a 200`() {
    runBlocking {
        val client = HttpClient()
        try {
            val response = client.get("https://jsonplaceholder.typicode.com/todos/1")
            response.status shouldBe HttpStatusCode.OK
        } finally {
            client.close()
        }
    }
}

IntelliJ had a squiggly line under the try, and the hint said "try-finally can be replaced with 'use()'". Um: sure. Can it? OK. Let's do that then. And it changed it for me to the code I initially showed.

I was bemused that the client.close() call had gone completely. I RTFMed a bit, and use is quite cool:

Executes the given block function on this resource and then closes it down correctly whether an exception is thrown or not.

It can be called on any object that implements the Closeable interface (although use is a Kotlin extension to this). If one looks at the implementation of Closeable.use we see what's going on (this is heavily elided):

fun use() {
    try {
        return block(this)
    } finally {
        try {
            close()
        } catch (closeException: Throwable) {
        }
    }
}

This use approach just makes sense.


Thoughts

In contrasting the very simple khttp approach to the Ktor approach, I prefer the khttp one. This reminds me of GuzzleHttp for PHP: it started off being a simple and useful and just… did HTTP requests. No async, no promises, it just got on with it. Then it decided async was all the rage so it took a very simple interface and made it overly complex for the task at hand (not to mention not backwards compatible). Ktor's approach seems similar. When I reflect upon how often I really need my HTTP requests to be done asynchronously to my mainline code, the answer is fucking never. Or so infrequently then I'm happy to write an async wrapper for the call if I need it.

But: we're already using Ktor, and it's more mature, and I don't have to retrograde to an old version of Java to use it. So Ktor and its unecessary complexity it is.

I'll try to do something more useful with this stuff tomorrow, but getting everything working (twice) took more time than I wanted to spend on this, so I'm stopping here for now.

The code for this is on GitHub @ /src/test/kotlin/junit/practical/WebServiceTest.kt.

Righto.

--
Adam

Thursday 6 October 2022

Kotlin: Data classes & componentN methods

G'day:

I needed to use a "data class" in my article last night ("Data-driven tests in JUnit and Kotest (and starting with TestBox & PHPUnit)" (see the TestCase class in the Kotest example), and I had no idea what one was, so I figured I should check this out today. TBH there's not much to these so I'm gonna quote a bit from the docs and show the tests that GitHub Copilot wrote for me, and… erm probably offer a bit of editorial comment I guess.

Oh, hey: I only mention Copilot because it did an amazing job of working out what I wanted to do with very little input, so I thought I'd use its code this evening. This is not good for me learning as I need to write the code to absorb whatever it is I am writing the code to do.

So what's a data class. I'll pinch some ofthe docs ("[cough]fair use[cough]"):

It is not unusual to create classes whose main purpose is to hold data. In such classes, some standard functionality and some utility functions are often mechanically derivable from the data.

I guess they're just second-guessing common usage patterns, and helping out. A data class has a bunch of method implicitly created, and that's best seen in some tests. These are the tests Copilot wrote for me:

class DataClassTest : DescribeSpec({
    describe("Data classes") {
        data class Person(val name: String, val age: Int)
        
        val person = Person("John", 30)

        it("should have a toString() method") {
            person.toString() shouldBe  "Person(name=John, age=30)"
        }

        it("should have a copy() method") {
            person.copy() shouldBe person
            person.copy(name = "Jane") shouldBe Person("Jane", 30)
            person.copy(age = 31) shouldBe Person("John", 31)
        }

        it("should have a componentN() method") {
            val (name, age) = person
            name shouldBe "John"
            age shouldBe 30
        }

        it("should have an equals() method") {
            person shouldBe Person("John", 30)
            person shouldNotBe  Person("Jane", 30)
            person shouldNotBe Person("John", 31)
        }

        it("should have a hashCode() method") {
            person.hashCode() shouldBe  -1781121024
        }
    }
})

IntelliJ created the class DataClassTest {} bits, and I typed the bits indicated, and Copilot did everything else. Yeah. I typed in "Desc" and Copilot nailed the toString, copy and componentN method tests, and then I needed to prompt it with "it("should have an ", and it went "oh you want tests for these other two as well: okeydoke". Obviously it's not noticed my penchant for using Maori words or notable NZ women as my test data, but it's done a good job. Well: I dunno about the hashCode test - it's a bit "hard-code-y" - but I can't come up with a better one (update: and indeed by the time I had finished writing all this, the test was failing cos the hashcode had changed, so I ditched that test).

The only interesting method in this lot is the componentN one. Kotlin allows destructuring declarations as per the test, ie: val (name, age) = person sets both variables name and age from the person object. To do this the object needs componentN methods. Note that this doesn't mean there's a method called componentN. It means there are methods component1, component2 (etc). In my case just those two. Here's another test to demonstrate that:

it("should have discrete componentN() methods") {
    person.component1() shouldBe "John"
    person.component2() shouldBe 30
}

If I try to write component3, I get told off by IntelliJ and the compiler:

Note also though that Copilot did its best to come up with an expectation for me. Also Copilot wrote the rest of the test for me:

I wanted to check if I could implement concrete componentN methods, so started typing again: "it("is possible to hand-crank one's own componentN methods?"), and Copilot did the rest:

it("is possible to hand-crank one's own componentN methods?") {
    class Person(val name: String, val age: Int) {
        operator fun component1() = name
        operator fun component2() = age
    }
    val person = Person("John", 30)
    val (name, age) = person
    name shouldBe "John"
    age shouldBe 30
}

Not that this time it's just a normal class, not a data class specifically. For completeness I want to make sure this isn't all smoke and mirrors, and actually have my own bespoke logic in the componentN methods. Copilot helps again, understanding 50% of what I'm asking for:

It's showing the ordering test working: the properties of the class are defined as name, age, and the component1 and component2 methods are the other way around. Cool. But it didn't get what I meant about customised logic. But fair-dos: it can't know what logic I want. I'll tweak it:

it("can have customised ordering and logic in the componentN methods") {
    class Person(val name: String, val age: Int) {
        operator fun component1() = "Age: $age"
        operator fun component2() = "Name: $name"
    }
    val person = Person("John", 30)
    val (age, name) = person
    name shouldBe "Name: John"
    age shouldBe "Age: 30"
}

All good. OK, so can I have more componentN methods than I have properties?

it("can have customised ordering and logic in the componentN methods") {
    class Person(val name: String, val age: Int) {
        operator fun component1() = "Age: $age"
        operator fun component2() = "Name: $name"
        operator fun component3() = "Name: $name, Age: $age"
    }
    val person = Person("John", 30)
    val (age, name, both) = person
    name shouldBe "Name: John"
    age shouldBe "Age: 30"
    both shouldBe "Name: John, Age: 30"
}

Yes. Yes it can. Copilot came up with the suggested implementation of component3 btw. And if I started typing in a component4 (well: I only needed to type "operator "), it also had a suggestion for that (operator fun component4() = "Age: $age, Name: $name"). In the context it's in, that's a sensible suggestion I reckon.

Right that's another short one. And a bit more about Copilot than I expected it to be, but hey. I think the componentN method stuff was interesting.

The code for this is @ /src/test/kotlin/kotest/language/classes/DataClassTest.kt.


Update on 2022-10-07 (the following day). I remembered I was supposed to be practising JUnit tests not Kotest ones, so I sat down to reimplement these same tests using JUnit-style instead:

@DisplayName("Tests of data classes")
internal class DataClassTest {

    @Test
    @DisplayName("should have a toString() method")
    fun testToString() {
        data class Person(val name: String, val age: Int)
        val person = Person("John", 30)
        assertEquals("Person(name=John, age=30)", person.toString())
    }

    @Test
    @DisplayName("should have a copy() method")
    fun testCopy() {
        data class Person(val name: String, val age: Int)
        val person = Person("John", 30)
        assertEquals(person, person.copy())
        assertEquals(Person("Jane", 30), person.copy(name = "Jane"))
        assertEquals(Person("John", 31), person.copy(age = 31))
    }

    @Test
    @DisplayName("should have a componentN() method")
    fun testComponentN() {
        data class Person(val name: String, val age: Int)
        val person = Person("John", 30)
        val (name, age) = person
        assertEquals("John", name)
        assertEquals(30, age)
        assertEquals("John", person.component1())
    }

    @Test
    @DisplayName("should have discrete componentN() methods")
    fun testDiscreteComponentN() {
        data class Person(val name: String, val age: Int)
        val person = Person("John", 30)
        assertEquals("John", person.component1())
        assertEquals(30, person.component2())
    }

    @Test
    @DisplayName("should have an equals() method")
    fun testEquals() {
        data class Person(val name: String, val age: Int)
        val person = Person("John", 30)
        assertEquals(Person("John", 30), person)
        assertEquals(Person("Jane", 30), person.copy(name = "Jane"))
        assertEquals(Person("John", 31), person.copy(age = 31))
    }

    @Test
    @DisplayName("is possible to hand-crank one's own componentN methods?")
    fun testHandCrankedComponentN() {
        class Person(val name: String, val age: Int) {
            operator fun component1() = name
            operator fun component2() = age
        }
        val person = Person("John", 30)
        val (name, age) = person
        assertEquals("John", name)
        assertEquals(30, age)
    }

    @Test
    @DisplayName("can have customised ordering and logic in the componentN methods")
    fun testCustomisedComponentN() {
        class Person(val name: String, val age: Int) {
            operator fun component1() = "Age: $age"
            operator fun component2() = "Name: $name"
            operator fun component3() = "Name: $name, Age: $age"
        }
        val person = Person("John", 30)
        val (age, name, nameAge) = person
        assertEquals("Age: 30", age)
        assertEquals("Name: John", name)
        assertEquals("Name: John, Age: 30", nameAge)
    }
}

I have indicated the bits of code that I wrote (IntelliJ populated the baseline class skeleton for me). Copilot did the rest. It worked out I was doing a JUnit version of the existing Kotest tests! Amazing.

This file is on GitHub @ /src/test/kotlin/junit/language/classes/DataClassTest.kt.


Righto.

--
Adam

Wednesday 5 October 2022

Data-driven tests in JUnit and Kotest (and starting with TestBox & PHPUnit)

G'day:

One thing I did not look at in any of my examinations of Kotest, and then JUnit5 was how to have data-driven tests in each platform. I'm going to start with how I'd've historically approached this task in a coupla frameworks I've used in the past.

TestBox

This is so easy to do in CFML I have not bothered to find out if TestBox has a native / idiomatic way of doing this.

describe("some tests", () => {

    numbers = {
        "one" = "tahi",
        "two" = "rua",
        "three" = "toru",
        "four" = "wha"
    }

    testCases = [
        {input="one", expected="tahi"},
        {input="two", expected="rua"},
        {input="three", expected="toru"},
        {input="four", expected="wha"}
    ]

    testCases.each((testCase) => {
        it("should return #testCase.expected# when passed #testCase.input#", () => {
            expect(numbers[testCase.input]).toBe(testCase.expected)
        })
    })
})

I loop over an array of cases, calling it with each variant.


PHPUnit

PHPUnit has a slightly clunkier approach, but gets there:

class DataProviderTest extends TestCase
{

    public function setUp() : void
    {
        $this->numbers = [
            "one" => "tahi",
            "two" => "rua",
            "three" => "toru",
            "four" => "wha"
        ];
    }

    /** @dataProvider provideCasesForNumberMapperTests */
    public function testNumberMapper($input, $expected)
    {
        $this->assertEquals($this->numbers[$input], $expected);
    }

    public function provideCasesForNumberMapperTests()
    {
        return [
            ["input" => "one", "expected" => "tahi"],
            ["input" => "two", "expected" => "rua"],
            ["input" => "three", "expected" => "toru"],
            ["input" => "four", "expected" => "wha"]
        ];
    }
}

Same principle, except the iteration over the test cases specified in the data provider is handled internally by PHPUnit.

As an aside, I am pretty pleased with a small addition to the test output that PHPUnt has at the moment:

adam@DESKTOP-QV1A45U:/mnt/c/temp/phpunit_test$ vendor/bin/phpunit
PHPUnit 9.5.25 #StandWithUkraine

....                                                                4 / 4 (100%)

Time: 00:00.100, Memory: 6.00 MB

OK (4 tests, 4 assertions)

Kotest (Data Driven Testing)

Kotest is better than PHPUnit, but isn't as straight-forward as TestBox:

class DataDrivenTest : DescribeSpec({
    describe("Data-driven tests") {
        val numbers = mapOf(
            Pair("one", "tahi"),
            Pair("two", "rua"),
            Pair("three", "toru"),
            Pair("four", "wha")
        )

        data class TestCase(val input: String, val expected: String)
        withData(
            TestCase("one", "tahi"),
            TestCase("two", "rua"),
            TestCase("three", "toru"),
            TestCase("four", "wha")
        ) { (input, expected) -> numbers[input] shouldBe expected }
    }
})

It's pretty compact though. Here we need to add that data class (I have not looked at the difference between a "data class" and a "class that just has properties" yet: I had better). The iteration over the test data is intrinsic to the withData function, which takes a lambda that receives the test data unpacked as separate values, and is the actual test.

When these are run, they show as individual cases in the runner output (ie: within IntelliJ):

And in the HTML test report:

That's pretty clear.


JUnit (JUnit 5 User Guide › 2.18. Dynamic Tests)

This is pretty easy too (I was expecting some clunky Java-esque monster here, but no):

class DataDrivenTest {

    private val numbers = mapOf(
        Pair("one", "tahi"),
        Pair("two", "rua"),
        Pair("three", "toru"),
        Pair("four", "wha")
    )

    @TestFactory
    fun `Data-driven tests`() = listOf(
        "one" to "tahi",
        "two" to "rua",
        "three" to "toru",
        "four" to "wha"
    ).map { (input, expected) ->
        DynamicTest.dynamicTest("numbers[$input] should be $expected") {
            numbers[input] shouldBe expected
        }
    }
}

This is pretty similar to TestBox really. One needs that @TestFactory annotation to identify the function as - pretty much - a data provider, then one maps that as dynamicTest calls, which take a label and the lambda for the test (both of which have the data availed to them).

The test output is a bit clearer in this case, as we get to specify the specific test case label.

In IntelliJ:

And HTML test report:


All in all I'm pretty happy with both approaches here - Kotest's and JUnit's. I have to say I think I prefer the JUnit approach in this case. There's not much in it, that said.

The code from this article is at /src/test/kotlin/kotest/system/kotest/DataDrivenTest.kt and /src/test/kotlin/junit/system/junit/DataDrivenTest.kt. I have to concede I did not bother to save the CFML or PHP code. Ooops.

Righto.

--
Adam

Sunday 25 September 2022

Kotlin: looking at JUnit instead of Kotest

G'day:

In the rest of my Kotlin articles thusfar I've been using Kotest for my testing. I went that direction instead of JUnit as the mindset behind xUnit style tests always seems to steer one towards writing tests of the code, rather than writing test cases for features. I think the RSpec-style better helps one focus on features when describing what the test case implementation will be when one starts with a descriptive statement rather than a method name. Plus the test output is a lot more user-friendly seeing the test case descriptions rather than test method names. Another area that RSpec-style beats xUnit hands-down when it comes to code organisation, given one can categorise one's tests within a test file via nested describe blocks. Lastly I think the "expectation" style of testing values - eg: expect(actual).toBe(expected) or actual shouldBe expected - is easier to read and more logical than assertEquals(expected, actual). That assertEquals approach is how Yoda would write an expectation, not a human.

However JUnit is the baseline testing framework for Java (and by extension Kotlin) so I'm gonna have a look at it to see how it shapes up. I've got some confirmation bias going on with my opinions here, I freely admit this. But I should sideline those when making the call as to which testing framework we should use for my day job.


First test / config

I'm following along "Test code using JUnit in JVM – tutorial", although my target here is to re-implement the Kotest tests I have so far in JUnit. But I figure there's be some config stuff I'll need to do upfront, and the tutorial will guide me through that.

There's a dependency and a task to add in my build.gradle.kts file, but I already have those in place via my Kotest installation:

// ...
dependencies {
    testImplementation(kotlin("test"))
    testImplementation("io.kotest:kotest-runner-junit5:5.4.2")
    testImplementation("io.kotest:kotest-assertions-core:5.4.2")
    testImplementation("com.github.stefanbirkner:system-lambda:1.2.1")
    testImplementation("org.apache.commons:commons-lang3:3.12.0")
}

tasks.withType<Test>().configureEach {
    useJUnitPlatform()
}

(I've just noticed that Kotest uses the JUnit5 test runner already).

There's some JUnit support baked-in to IntelliJ (of course there is), so to create a test, apparently all I need to do is to select a method › right-click › Generate › Test… and IntelliJ will sort it out for me:

Apparently I have to install that JUnit5 library… I wonder how that differs from the dependency I have in the build file already? Let's press it and find out…

… it's also added these two dependencies now:

dependencies {
    testImplementation(kotlin("test"))
    testImplementation("io.kotest:kotest-runner-junit5:5.4.2")
    testImplementation("io.kotest:kotest-assertions-core:5.4.2")
    testImplementation("com.github.stefanbirkner:system-lambda:1.2.1")
    testImplementation("org.apache.commons:commons-lang3:3.12.0")
    testImplementation("org.junit.jupiter:junit-jupiter:5.8.1")
    testImplementation("org.junit.jupiter:junit-jupiter:5.8.1")
}

Looks like that's just JUnit5 internal housekeeping. Okey doke.

OK, I've renamed the suggested test class from MainKtTest to MainTest, and told it to go in the junit package in my app (so in src/test/kotlin/junit/MainTest.kt). Letting this run lands me with this skeleton:

package junit

import org.junit.jupiter.api.Test

import org.junit.jupiter.api.Assertions.*

internal class MainTest {

    @Test
    fun main() {
    }
}

It got the name of the test method wrong, but that's an easy fix (ie: it should be testMain) I sling a failing test into that to make sure the runner does it's thing properly:

@Test
fun testMain() {
    assertTrue(false)
}
org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at app//org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:55)
	at app//org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:40)
	at app//org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:35)
	at app//org.junit.jupiter.api.Assertions.assertTrue(Assertions.java:179)
	at app//junit.MainTest.testMain(MainTest.kt:11)  

All good. I wonder if I can right-click the top level of the test dir and run all tests: Kotest and JUnit ones:

 Test Results
     PropertiesTest
     kotest.MainTest
         Tests of Main class
             outputs G'day World & its args
             works OK with no args
     kotest.language.ScopeFunctionsTest
     kotest.language.classes.AbstractTest
     kotest.language.classes.ClassesTest
     kotest.language.classes.InheritanceTest
     kotest.language.collections.CollectionTest
     kotest.language.functions.FunctionSyntaxTest
     kotest.language.properties.BackingPropertiesTest
     kotest.language.properties.LateInitPropertiesTest
     kotest.language.types.FunctionsTest
     kotest.language.types.NumberTest
     kotest.language.types.strings.StringTest
     kotest.language.variables.VariablesTest
     kotest.system.SystemTest
     kotest.system.kotest.ConfigTest
     kotest.system.kotest.KotestTest
     kotest.system.kotest.MatcherTest
     MainTest
         testMain()

Cool! Yes it will.

OK, now to re-implement those two tests I need on main:

package junit

import com.github.stefanbirkner.systemlambda.SystemLambda
import main
import org.junit.jupiter.api.Test

import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.DisplayName

@DisplayName("Tests of Main class")
internal class MainTest {

    val EOL = System.lineSeparator()

    @Test
    @DisplayName("it outputs G'day World & its args")
    fun testMainOutputsCorrectly() {
        val testArgs = arrayOf("some arg", "some other arg")
        val output = SystemLambda.tapSystemOut {
            main(testArgs)
        }
        assertEquals("G'day World!${EOL}Program arguments: some arg, some other arg", output.trim())
    }

    @Test
    fun `works OK with no args`() {
        val testArgs = arrayOf<String>()
        val output = SystemLambda.tapSystemOut {
            main(testArgs)
        }
        assertEquals("G'day World!${EOL}Program arguments:", output.trim())
    }
}

I'm pleased with this, and a couple of my confirmation-bias-based concerns have been mitigated already:

When I run these tests, the output is pretty much on-point for what I want to see:

 Test Results
     Tests of Main class
         it outputs G'day World & its args
         it works OK with no args()

Second challenge solved: hierarchical tests

In my intro, one of the challenges I predicted xUnit tests would have compared to RSpec style ones is the lack of hierarchical organisation in xUnit tests. I have become used to this sort of thing (implementation code elided for brevity):

class SystemTest : DescribeSpec({
    describe("Tests of kotest installation") {
        it("should return the size of a string") {
            // …
        }
        it("should test for the prefix of a string") {
            // …
        }
    }
    describe("tests of system-lambda") {
        it("checks system-lambda is working OK") {
            // …
        }
    }
})

Here I have two distinct sets of tests in the one test class. This aids code organisation, as well as making the output easier to deal with:

 Test Results
     kotest.system.SystemTest
         Tests of kotest installation
             should return the size of a string
             should test for the prefix of a string
         tests of system-lambda
             checks system-lambda is working OK

Last time I checked xUnit, this wasn't a thing. But I'm pleased to discover it is a thing with JUnit5:

class SystemTest {

    @Nested
    @DisplayName("Tests of JUnit installation")
    inner class JUnitInstallationTest {
        @Test
        fun `the length of the string should be 5`() {
            assertEquals(5, "hello".length)
        }

        @Test
        fun `the string should start with "wor"`() {
            assertTrue("world".startsWith("wor"))
        }
    }

    @Nested
    @DisplayName("Tests of system-lambda")
    inner class SystemLambdaTest {
        @Test
        fun `system-lambda should be able to capture stdout`() {
            val testString = "string to capture"
            val output = SystemLambda.tapSystemOut {
                print(testString)
            }
            assertEquals(testString, output)
        }
    }
}
 Test Results
     SystemTest
         Tests of JUnit installation
             the length of the string should be 5()
             the string should start with "wor"()
         Tests of system-lambda
             system-lambda should be able to capture stdout()

This is all down to that @nested annotation, and nesting classes.

There's actually a win specifically for JUnit here too. Using the it syntax with RSpec-style tests, the pedant in me always tries to make a grammatically-correct sentence out of the function call, eg:

it("is a grammatically correct sentence that describes the test case", () => {}))

This often works well. But sometimes it's just daft to wangle a correct-sounding test description starting with "it". As the JUnit descriptions or back-ticked method names have no constraint that the case description "must" start with it, I can actually use better test case labels here. Cool. This is minor, and down to me being a pedant I know. But still: I like it.

One thing to note about that though: I get a warning on the `the string should start with "wor"` test:

w: […]/src/test/kotlin/junit/system/SystemTest.kt: (20, 13): Name contains characters which can cause problems on Windows: "

So I better keep an eye on that. The test did run though (on both Windows and Linux).


Third challenge: conditional tests

Kotest can do this sort of thing:

it ("only runs on linux").config(enabled = SystemUtils.IS_OS_LINUX) {
    SystemUtils.IS_OS_LINUX shouldBe true
}

That test only runs when the OS is Linux. I needed to do this as I had a test that needed to deal with OS-specific line-endings (Linux: LF; Windows: CRLF). How to do this in JUnit? Excuse me whilst I google etc.…

[five minutes pass]

…OK, it's pretty easy, just use @EnabledOnOs:

@Test
@DisplayName("The test is only executed on Linux")
@EnabledOnOs(OS.LINUX)
fun testOnLinux() {
    assertTrue(SystemUtils.IS_OS_LINUX)
}

On Linux:

On Windows:


Random things

There's a few wee things that don't warrant much discussion, but they're stuff I tested in Kotest so wanna make sure I can do the same thing with JUnit.

fail and assertThrows<AssertionError>

@Test
fun `It has a fail method and its exception can be caught and asserted against`() {
    val exception = assertThrows<AssertionError> {
        fail("This is a failure")
    }
    assertEquals("This is a failure", exception.message)
}

Messages on assertions

These are done with clues in Kotest:

withClue("this is the clue") {
    fail("this is a failure message")
}

And with the message argument on the assertion with JUnit:

fun `A message can be supplied with the assertion to make failures clearer`() {
    val exception = assertThrows<AssertionError> {
        assertEquals(1, 2, "the two numbers should be equal")
    }
    assertEquals("the two numbers should be equal ==> expected: <1> but was: <2>", exception.message)
}

@TestFactory

I had a brain fart with one of my Kotest tests and was gonna test one thing, but then tested something different. So let's not worry about the Kotest version of this. But what I was wanting to do is to pass a list of inputs to a test and have the test run for each of them. Easy with JUnit:

@TestFactory
fun testPrimeFactorsOf210() = listOf(2, 3, 5, 7).map {
    DynamicTest.dynamicTest("Dividing by $it") {
        assertEquals(0, 210 % it)
    }
}
 testPrimeFactorsOf210()
     Dividing by 2
     Dividing by 3
     Dividing by 5
     Dividing by 7

Grouping multiple assertions with assertAll

This is the equivalent of Kotest's Soft assertions:

describe("Soft assertions") {
    it("can use soft assertions to let multiple assertions fail and report back on all of them") {
        val actual :Int = 15

        try {
            assertSoftly {
                actual.shouldBeTypeOf<Number>()
                withClue("should be 15.0") {
                    actual.equals(15.0).shouldBeTrue()
                }
                actual shouldBe 15
                actual shouldBe 16
            }
        } catch (e :MultiAssertionError) {
            assertSoftly(e.message) {
                shouldContain("1) 15 should be of type kotlin.Number")
                shouldContain("2) should be 15.0")
                shouldContain("expected:<true> but was:<false>")
                shouldContain("3) expected:<16> but was:<15>")
            }
        }
    }
}
})

JUnit version:

fun `It can use soft assertions to let multiple assertions fail and report back on all of them`() {
    val actual: Int = 15
    val exception = assertThrows<AssertionError> {
        assertAll(
            { assertEquals(Number::class, actual::class) },
            { assertEquals(15.0, actual) },
            { assertEquals(15, actual) }, // this one is OK
            { assertEquals(16, actual) }
        )
    }
    assertTrue(exception.message!!.contains("expected: <class kotlin.Number> but was: <class kotlin.Int>"))
    assertTrue(exception.message!!.contains("expected: <15.0> but was: <15>"))
    assertTrue(exception.message!!.contains("expected: <16> but was: <15>"))
}

I'm gonna pause things here. I've got other stuff to do this afternoon, plus I'm happy that JUnit5 meets the organisation requirements I expect from a testing framework at this point. I'm not so happy with its native assertions library as they seem pretty limited, and I still think the assertEquals(expected, actual) approach to things is clunky. The next thing I need to look at is options for extending the assertion suite, and I have already seen there's a coupla options. I reckon that will be an article to itself, though.

The code for this lot is up on GitHub @ /src/test/kotlin/junit.

Righto.

--
Adam