Saturday 3 September 2022

Kotlin: the next morning learning Kotlin stuff

G'day:

Whether you like it or not, I'm just gonna continue on from yesterday's random effort: Kotlin: an afternoon learning Kotlin stuff. Hey: no-one's making you read this stuff.


Next koans exercise: trimMargin

TBH I've never needed functionality like this - it seems very edge-case-y? - and am quite perplexed as to why this is put forward so early in the koans exercises. But… well here it is:

describe("trimMargin tests") {
    it("does what it says on the tin") {
        val testString = """
            | has | as a margin character
            | | is the default btw
        """.trimMargin()

        testString shouldBe " has | as a margin character\n | is the default btw"
    }

    it("handles a different margin") {
        val testString = """
            # has "# "
            # as a margin value
        """.trimMargin("# ")

        testString shouldBe "has \"# \"\nas a margin value"
    }
}

Note in the first one it only strips the indentation and the |, not the space after the |. There's no reason why it should, but I did wonder when writing the test.

I guess "better to have, and not need; than to need, and not have". But I hope if Kotlin has taken to the time to add this, then they've also taken the time to pretty much add every other bloody thing one might want to do to a string first. I can only suppose it's quite closely-related to trimIndent (see y/day's article for that one), so kinda makes sense (ish) to have this too. Can you tell I remain unconvinced? OK: moving on.


String interpolation / template expressions in strings

The koans exercise relating to trimMargin also touched on template expressions:

describe("template expression tests") {
    it("resolves a variable in a string literal") {
        val name = "Zachary"

        "G'day $name" shouldBe "G'day Zachary"
    }

    it("resolves a variable in a raw string") {
        val name = "Joe"

        """
            G'day $name
        """.trimIndent() shouldBe "G'day Joe"
    }

    it("can use curly braces to disambiguate where the variable name ends") {
        val prefix = "SOME_PREFIX_"
        "${prefix}REST_OF_STRING" shouldBe "SOME_PREFIX_REST_OF_STRING"
    }

    it("can take more complicated expressions") {
        val name = "Zachary"

        "G'day ${name.uppercase()}" shouldBe "G'day ZACHARY"
    }

    it("can take blocks of code provided they resolve to a string?") {
        val name = "Joe"
        val case = "lower"

        "G'day ${if (case == "upper"){name.uppercase()} else {name.lowercase()}}" shouldBe "G'day joe"
    }
}

Now I would never advocate doing what I have done in that last example; I was just checking to see if it would work.


Handling exceptions in Kotest tests

Whilst writing that "can use curly braces to disambiguate where the variable name ends" test above, I was not sure if using a "wrong" variable name in the template expression might be a runtime thing that I could catch in test. As it turns out it's checked at compile time so I could not test for it, buit it got me looking into how to expect exceptions in a test.

Unsurprisingly Kotest has good support for this (see Assertions › Exceptions).

package system.kotest

import io.kotest.assertions.throwables.shouldNotThrow
import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.assertions.throwables.shouldThrowAny
import io.kotest.core.spec.style.DescribeSpec
import java.lang.AssertionError

class MatcherTest : DescribeSpec({

    describe("tests of shouldThrow~ variants") {

        class MySpecificException(message: String) : Exception(message)
        class MyDifferentException(message: String) : Exception(message)

        it("expects an exception") {
            shouldThrowAny {
                throw Exception("any old exception")
            }
        }

        it("expects a specific exception") {
            shouldThrow<MySpecificException> {
                throw MySpecificException("My specific exception")
            }
        }

        it("enforces a specific exception") {
            shouldThrow<AssertionError> {
                shouldThrow<MySpecificException> {
                    throw Exception("My specific exception")
                }
            }
        }

        it("expects an exception to not be thrown") {
            shouldNotThrowAny {
                // NOP
            }
        }

        it("expects a specific exception to not be thrown") {
            shouldNotThrow<MySpecificException> {
                // NOP
            }
        }

        it("expects a specific exception to not be thrown, but bubbles up any different exception") {
            shouldThrow<MyDifferentException> {
                shouldNotThrow<MySpecificException> {
                    throw MyDifferentException("My different exception")
                }
            }
        }

        it("deals with test failures when using the special handling of shouldNotThrow<Any> above") {
            shouldThrow<AssertionError> {
                shouldNotThrow<MySpecificException> {
                    throw MySpecificException("This specific exception is NOT expected, so should cause an AssertionError (which the test expects, so still passes")
                }
            }
        }
    }
})

One thing to note here is how I am using this strategy to test "failing" behaviour in assertion:

it("enforces a specific exception") {
    shouldThrow<AssertionError> {
        // code that SHOULD cause an assertion failure here
    }
}

So in this test:

it("enforces a specific exception") {
    shouldThrow<AssertionError> {
        shouldThrow<MySpecificException> {
            throw Exception("My specific exception")
        }
    }
}

I am testing that shouldThrow<MySpecificException> isn't fulfilled - the code the assertion is testing (throw Exception("My specific exception") doesn't throw a MySpecificException, even though that's what the assertion needs). So a "working" test here is that the test code does actually throw an AssertionError. Hopefully that makes sense. It looks confusing because I'm using the testing framework to test its own assertion behaviour. I'm not sure that paragraph make things clearer or just even worse. Sorry.

Anyhoo, everything works exactly how I'd expect.

Defining a class anywhere

You might have noticed this in the test code above:

class MatcherTest : DescribeSpec({

    describe("tests of shouldThrow~ variants") {

        class MySpecificException(message: String) : Exception(message)
        class MyDifferentException(message: String) : Exception(message)

        // ...

        it("expects a specific exception") {
            shouldThrow<MySpecificException> {
                throw MySpecificException("My specific exception")
            }
        }

        // ...

        it("expects a specific exception to not be thrown, but bubbles up any different exception") {
            shouldThrow<MyDifferentException> {
                shouldNotThrow<MySpecificException> {
                    throw MyDifferentException("My different exception")
                }
            }
        }

        // ...
    }
})

I'm able to define classes - seemingly - anywhere I like (see Nested and inner classes). I need a coupla specifically-typed Exception objects in my tests, so I can just define them where I need them. Before I had a second test needing that MyDifferentException instance, I had that class declaration within the it callback for the test itself. I like this.

Null safety & safe calls

This stuff is compile-time safety so I can't actually write tests for it, but I'll just repeat the koan exercise here (not sure this is a copyright violation? Maybe if I just say "fair use!!" I'll be fine ;-))

The want me to convert this (Java code):

public void sendMessageToClient(
    @Nullable Client client,
    @Nullable String message,
    @NotNull Mailer mailer
) {
    if (client == null || message == null) return;

    PersonalInfo personalInfo = client.getPersonalInfo();
    if (personalInfo == null) return;

    String email = personalInfo.getEmail();
    if (email == null) return;

    mailer.sendMessage(email, message);
}

To Kotlin code that fits into this function declaration:

fun sendMessageToClient(
        client: Client?, message: String?, mailer: Mailer
) {
    TODO()
}

class Client(val personalInfo: PersonalInfo?)
class PersonalInfo(val email: String?)
interface Mailer {
    fun sendMessage(email: String, message: String)
}

And my code to leverage the safe call operator (?.) so that it only needs the one if statement. This is reasonably familiar from CFML territory (and I notice PHP8 now has the null-safe opeator as well (?-> (yes, really)), so hopefully I don't mess-up this exercise…

Before we get to how I did mess up this exercise, I'll note this cool thing IntelliJ does. I copy and pasted the body of the Java method over the TODO() place holder, and IntelliJ came up with this:

This kinda felt like cheating, but I wanted to see what it did, and it came up with this:

fun sendMessageToClient(
        client: Client?, message: String?, mailer: Mailer
) {
    if (client == null || message == null) return

    val personalInfo: PersonalInfo = client.getPersonalInfo() ?: return

    val email = personalInfo.email ?: return

    mailer.sendMessage(email, message)
}

I felt relieved about not cheating cos this doesn't solve the "only one if statement" constraint to my liking. I mean it's only got one if, sure, but it's also got two elvis operator bail-outs, and I didn't want those: that's not in-keeping with the intent of the exercise.

More importantly, IntelliJ was as bemused by the getPersonalInfo call as I was. Where's the getPersonalInfo coming from? They provide the implementation of Client, and it's just this:

class Client(val personalInfo: PersonalInfo?)

I kinda guessed that there was some way I was unaware of to enable synthesised accessor methods on the class properties so a getPersonalInfo was available, but try as I might I could not find how to do it, so I gave up and cheated. The answer they were after was this:

fun sendMessageToClient(
        client: Client?, message: String?, mailer: Mailer
) {
    val email = client?.personalInfo?.email
    if (email != null && message != null) {
        mailer.sendMessage(email, message)
    }
}

Yeah all right. I was being too literal. There was not a way of being able to use getPersonalInfo to access personalInfo, they just expected me to access the thing directly. Looking at it now, IntelliJ's Java -> Kotlin conversion even did this for this statement:

// Java
String email = personalInfo.getEmail();

// converted to Kotlin
val email = personalInfo.email

Sigh. But I wonder why IntelliJ managed to automatically convert that one, but not the getPersonalInfo one? Interesting.

Anyway, I feel dumb now. I mean dumber than before. Grrr.

BTW I did try to just implement a getPersonalInfo method in the Client class just so I could get my example moving, and got this interesting error:

Reading that made me think "OK it's telling me there already is a getPersonalInfo method. OK so WhyTF doesn't it work then?" After cheating and whilst writing these coupla paragraphs up, I did a google and found this on Stack Overflow, in answer to "How to overcome "same JVM signature" error when implementing a Java interface?"):

You could use @JvmField for instructs the compiler not generate getter/setter…

I'm guessing the story is that under the hood Kotlin has generated explicit getters for me for my class's properties, so there's no need (or want) for me to make explicit ones. And Kotlin uses those to enable direct access to the property via client?.personalInfo. Clearly more reading needed on my part here.

I did come across the bit in the Kotlin docs about being implement implicit accessor methods for properties (Getters and setters). I'm sure this is tied into that error message I was getting too. I'm gonna have a look at that stuff… next time. Yes, sorry: there will be a "next time". Probably tomorrow.

OK. I'm gonna treat this dumb feeling I have with beer. Because that'll work.

Righto.

--
Adam