Sunday 21 August 2022

Kotlin / Gradle / Kotest random exploration

G'day:

I am currently wondering whether this one will ever see light-of-day. It's very much what it says on the tin: I've been messing with my Kotlin ecosystem again, and I've tried and suceeded with some random things, so thought I'd write them down. There's nothing groundbreaking here, I warn you. But it's all new to me.

This follows on from the previous article: "Kotlin: creating a project and getting some code (and tests) to run". This is the very next time I have sat down and opened IntelliJ and gone "OK so now what?".


What I actually sat down to do was to work out what all the shit in the build.gradle.kts file actually means, so I had a google and found Gradle Kotlin DSL Primer. This seemed like a good start. However I only got as far as a mention of "Run ./gradle tasks to get more details" and I thought "Oh yeah, I should probably know how to use the thing from the shell as well as just pushing buttons in the IDE", so dropped down to my shell and ran ./gradlew.bat tasks (note the docs said gradle not gradlew but I did not notice that at first. And the .bat bit is just cos I'm running this in Powershell not in Bash).

I googled the difference between gradle and gradlew, and found this: gradle vs. gradlew – what’s the difference?. Basically gradle runs the underlying Gradle application, whereas gradlew runs the "Gradle Wrapper" which is the app's stand-alone Gradle runner which bundles up a bunch of app-related stuff,a nd indeed will install Gradle if it's not already installed. It's just a "self-containment" sort of thing. Best to read the article to get that clear.

Anyhow, running gradlew.bat tasks output a bunch of stuff which I won't be going back to in this article (told you it was random):

Because I noticed the magic word:

test - Runs the test suite.

(You have to picture me sitting at my keyboard clapping twice to myself and voicing "yay" with more glee than I really ought to be admitting to).


Right so let's run my tests:

PS C:\src\kotlin\scratch> .\gradlew.bat test

BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 up-to-date
PS C:\src\kotlin\scratch>

OK, that's not as exciting as it could be. Indeed I dunno even if it ran some tests or not. Let's red-green-refactor this: I'll break a test…

PS C:\src\kotlin\scratch> .\gradlew.bat test

> Task :test

MainTest > Tests of Main class > MainTest.outputs G'day World & its args FAILED
    io.kotest.assertions.AssertionFailedError at MainTest.kt:12

5 tests completed, 1 failed

> Task :test FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///C:/src/kotlin/scratch/build/reports/tests/test/index.html

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 4s
3 actionable tasks: 2 executed, 1 up-to-date
PS C:\src\kotlin\scratch>

Whoa. OK. I guess if the build is green: all tests pass. If not: it'll let me know.

But what's this report it speaks of:

> There were failing tests. See the report at: file:///C:/src/kotlin/scratch/build/reports/tests/test/index.html

Let's have a look at that:

I like that. And I can drill down into my failed test too:

Let's fix the test and look at the passing report:


Cool. My next random thought was "hrmph: PowerShell". I don't like PowerShell. Largely cos I've never bothered to learn to use it, either just using the old school Windows shell, or more often when I'm needing to use a shell it's in a *nix environment, and I use Bash. So let's see if I can do all this in Bash as well:

adam@DESKTOP-QV1A45Uadam@DESKTOP-QV1A45U:~$ cd /mnt/c/src/kotlin/scratch/
adam@DESKTOP-QV1A45U:/mnt/c/src/kotlin/scratch$ ./gradlew test
Starting a Gradle Daemon (subsequent builds will be faster)

> Task :test

MainTest > Tests of Main class > MainTest.outputs G'day World & its args FAILED
    io.kotest.assertions.AssertionFailedError at MainTest.kt:12

MainTest > Tests of Main class > MainTest.works OK with no args FAILED
    io.kotest.assertions.AssertionFailedError at MainTest.kt:19

5 tests completed, 2 failed

> Task :test FAILED

Oh. :-(

However checking the report and drilling down into the failures, it made sense:

io.kotest.assertions.AssertionFailedError:
(contents match, but line-breaks differ; output has been escaped to show line-breaks)
expected:<G'day World!\r\nProgram arguments: some arg, some other arg>
but was:<G'day World!\nProgram arguments: some arg, some other arg>

It's the old "Windows uses CRLF / *nix uses LF" thing, and my test has hard-coded CRLF in it:

output.trim() shouldBe "G'day World!\r\nProgram arguments: some arg, some other arg"

I'm sure there's a constant or something in Java that is the current EOL marker for the given operating system. I googled around and found this: Stack Overflow › Is there a Newline constant defined in Java like Environment.Newline in C#?, and the answer is System.lineSeparator() (integrated into my tests now):

class MainTest : DescribeSpec ({
    describe("Tests of Main class") {
        val EOL = System.lineSeparator()

        it("outputs G'day World & its args") {
            val testArgs = arrayOf("some arg", "some other arg")
            val output = SystemLambda.tapSystemOut {
                main(testArgs)
            }
            output.trim() shouldBe "G'day World!${EOL}Program arguments: some arg, some other arg"
        }
        it("works OK with no args") {
            val testArgs = arrayOf<String>()
            val output = SystemLambda.tapSystemOut {
                main(testArgs)
            }
            output.trim() shouldBe "G'day World!${EOL}Program arguments:"
        }
    }
})

Right so lets see if the tests work in both environments now. Powershell:

BUILD SUCCESSFUL in 1s

Bash:

BUILD SUCCESSFUL in 1s

Cool.


Oh I need to back up a bit. I also had to go find out how to do interpolated strings in Kotlin as well for that:

output.trim() shouldBe "G'day World!${EOL}Program arguments:"

I googled-up this page: How Does String Interpolation Work in Kotlin?, which says one just prefixes the variable name with $. As my EOL variable usage is hard-up against another string literal, I needed to (guess) that one should surround it in curly braces to make it clear where variable name stopped and string literal started. Otherwise I got this: Unresolved reference: EOLProgram.


The last thing I thought about was "but what if there was no generic solution for the test, and I had to only conditionally run a test given some criterion?" Kotest can do this: Conditional tests with enabled flags. I put together a new test to show myself this enabled flag in action:

import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import org.apache.commons.lang3.SystemUtils

class KotestTest : DescribeSpec( {
    describe("Testing test conditionality") {
        it ("only runs on linux").config(enabled = SystemUtils.IS_OS_LINUX) {
            SystemUtils.IS_OS_LINUX shouldBe true
        }
    }
})

To use SystemUtils I had to include Apache Commons as a dependency in build.gradle.kts:

dependencies {
    implementation("org.apache.commons:commons-lang3:3.12.0")

    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")
}

BTW I'm finding the package name by just googling something like "apache commons maven", and it leads me to a page like this: https://mvnrepository.com/artifact/org.apache.commons/commons-lang3/3.12.0:

That even gives me the statement to use in my build file. In hindsight I should probably only be using that as a test dependency, so I'll change that:

testImplementation("org.apache.commons:commons-lang3:3.12.0")

Also note that after discussion with SeanC in the Working Code Podcast Discord channel, I have stopped using the ranged constraints for my dependencies (eg: something like 3.12.+), but using concrete ones (eg: something like 3.12.0) like I have here. I've changed all the other ones I alerady had, too. This is just "the Java way" apparently. Duly noted.

OK so now I run the tests, and check if the conditionality works. The difference is best seen in the test report:

Bash:

Powershell:

Excellent.


At this point I had decided I had had enough, and also thought maybe I could get a blog article out of it (still not sure I have, but hey… the bar is set pretty low around here so I'm gonna press "publish" anyhow).

Just to recap, this is what I learned in this exercise:

  • I found that docs page on the Kotlin Gradle DSL which I will come back to (Gradle Kotlin DSL Primer),
  • I had the most superficial of looks at the gradlew shell runner thingey.
  • I got a better understanding of gradle vs gradlew.
  • I found out how to run the tests on same.
  • And discovered that auto-generated HTML test results page.
  • The tests will run just fine on Powershell and Bash.
  • System.lineSeparator() contains the platform-specific line ending.
  • How to do interpolated strings, eg: "G'day World!${EOL}Program arguments:".
  • How to set a a condition on a Kotest test to control whether it's ignored.
  • How to find what the dependency string and version is for the implementation/testImplementation statement is, from Maven.

There's no rocket science there, nor anything complicated. But I've only just started looking at this stuff so can't expect too much. Everything was really easy to find, and worked with a minimum of fuss, too.

Let me know if this article is too "all over the place" or it held any interest or not. I actually found it quite useful myself for formalising the stuff I had looked at, so I guess it's at least useful to me if not for anyone else.

Righto.

--
Adam


PS: Oh! I also learned how to pass args to my app when I run it. Cos like I didn't just test it, I also ran it:

adam@DESKTOP-QV1A45U:/mnt/c/src/kotlin/scratch$ ./gradlew run --args "first second third"

> Task :run
G'day World!
Program arguments: first, second, third  

PPS: the code for this is at https://github.com/adamcameron/kotlin_scratch/tree/1.1.