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

DRY: don't repeat yourself

G'day:

This should be a short one. I've had this text lying around for a while, wondering if I could spin it out to be a longer article somehow, bt I never managed to work out how. Then a few days ago I needed to point to something about "DRY" (so I didn't have to… erm… repeat myself), and got annoyed that I didn't have this article already. I guess articles don't need to be long, if the point itself doesn't need much discussion. So here goes.

Everyone has heard about the DRY Principle, I would hope. From that same article on Wikipedia:

The DRY principle is stated as "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system".

The articles goes further to explain it more thoroughly, but I think that's about as far as most intermediate-level devs ever read, and they get the wrong end of the stick. They perceive it to mean "never write the same code more than once",a dn they stick to that dogmatically, de-duping their code at every step and making an unholy mess of unnecessary complexity along the way. But this is not what the DRY Principle means. DRY is not about code, it is about complexity of concepts. Any complexity should be extracted/refactored/de-duplicated so as to reduce complexity. I just found a handy quote explaining DRY well:

the DRY Principle is actually about removing duplicate ideas and not duplicate code

Steven Solomon in Don't Repeat Yourself Is Misunderstood

If one takes three very similar pieces of simple functionality and extracts and combines them into one more complex piece of functionality that serves all three original usages in a generic fashion, that is likely to be increasing the complexity of the original code (and the extracted code). That's not a good application of the DRY principle. For one thing you now have three places in your codebase that are only similar, but have different variations, all coupled together. This violates the Single Responsibiltity Principle, for one thing.

Another way of saying much the same thing (now that I re-read it) is that just because multiple pieces of functionality seem superficially the same doesn't mean they are the same, and accordingly need to be the same implementation. For example if three pieces of code need measure off the minutes in a day, it's OK for each of them to store the value 1440. We do not gain anything from extracting that into a AmountsOfTime::MINUTES_IN_A_DAY constant. It's quite possibly just coincidence that each piece of code is using "minutes in a day" for whatever they are measuring at that point in time, and tightly coupling them together is not appropriate. One might be "minutes until the task should run again", the other might be "minutes to cache that thing", and the other might be… oh, I dunno: "minutes until the same time tomorrow" (OK that's dumb). Here the value is the same, and it's measuring the same thing, but the purpose of each is not actually inter-related. So it's OK to duplicate the simple concept of 1440 is the number of minutes in the day, however label it with why we need to know that value, not simply "what the value is". Digression: I guess this is actually the same motivation behind not writing comments that describe what the code does, eg:

Pointless waste of time:

// re-run the task in 1440min
reRun(theTask, 1440)

Actually useful:


// don't do this again for 1440min because it needs to report on the whole day's activity
reRun(theTask, 1440)

On the other hand if we have two pieces of code that define how we apply (random invented example not at all related to my previous job. Cough) the FX hedge to a value… we pretty much need that calculation - even though it's simply multiplying the input by a static multiplier - to be in only one place because it's a specific business rule, and it reduces complexity to have it in one place, and simply to call it from wherever it's needed.

class ForeignExchange {
    
    hedgeMultiplier = 1.05

    applyHedge(amount) {
        return amount * hedgeMultiplier
    }
}

The business rule is a simple multiplication expression, but it's to do with financial transactions, and its usage needs to be uniform (because customers notice and complain when it isn't. In theory. In this fictitious analaogy I am using here. [another cough]). Plus, as we found out: we needed to make the expression slightly more complex than that. Again this comes back to the Single Responsibility Principle.

I think a lot of intermediate-level devs get into the trap of premature optimisation, and this can cause a misapplication of the DRY Principle: as soon as they find themselves starting to write similar code a second time, they immediately refactor it into a common location. Personally I think the shape that a refactoring might take only starts to surface on the third replication of the same code. When one is looking at a sample size of two when considering refactoring, it's pretty difficult to tell if it's a pattern, or if it's just a coincidence. Wait a bit longer to see if the pattern forms before refactoring stuff (ie: applying the DRY Principle).

Right. So that's 50/50 stuff I had already written, and a few more thoughts. It's a pretty short article for me, but, well: this is all I had to say so I ain't gonna spin it out further.

Bottom line: it's OK for code to be duplicated, if the code is simple.

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

Friday, 23 September 2022

Kotlin: abstract classes, backing fields/properties, lateinit, and frustration

G'day:

I'm just continuing on in the Kotlin "Classes" docs, carrying on from my earlier article "Kotlin: investigating classes".


Abstract classes

I have pretty much run with the example in the docs just to see it in action. I don't wanna copy and paste stuff cos I don't learn so well just by looking at the code. Making myself remember what I need to type - even if it's just class syntax with a method or two - helps me learn it.

The thing the example had was to have an abstract class that has a concrete implementation of a method, extended by another abstract class that actually makes that method abstract again, followed by a concrete implementation class. To get my brain around that, I wrote it in Java first (all this experimentation is making my Java better too btw: I'm writing more Java-comparison code for these articles than I'm necessarily showing in them). My Java is limited to one-off class files with a main method though. I can't be arsed setting up a Java project in IntelliJ and working out how to get JUnit working, etc. Sorry. (Dammit I feel guilty now). Anyway, Java version:

public class TestAbstract {
    public static void main(String[] args){
        MyWildShape mws = new MyWildShape();
        mws.draw();
    }
}

abstract class Shape {
    public void draw() {
        System.out.println("Shape.draw"); // MyWildShape.draw
    }
}


abstract class WildShape extends Shape {
    abstract public void draw();
}  


class MyWildShape extends WildShape {
    @Override
    public void draw() {
        System.out.println("MyWildShape.draw");
    }
}

And the Kotlin version:

abstract class Shape {
    open fun draw() = print("Shape.draw")
}

abstract class WildShape : Shape() {
    abstract override fun draw()
}

class MyWildShape : WildShape() {
     override fun draw() = print("MyWildShape.draw")
}

class AbstractTest : DescribeSpec({
    describe("abstract test") {
        it("permits concrete subclasses to override abstract methods that are not overridden in the base class") {
            val mws = MyWildShape()
            val output = SystemLambda.tapSystemOut {
                mws.draw()
            }
            output shouldBe "MyWildShape.draw"
        }
    }
})

I wondered what the need for open was in that. Kotlin's been pretty good an letting boilerplate be inferrable, and this seemed like boilerplate. I found the answer in the docs:

By default, Kotlin classes are final – they can't be inherited. To make a class inheritable, mark it with the open keyword

Kotlin / Concepts / Classes and objects / Inheritance

As to why that's the case, I had to dig slightly deeper, but I found a discussion on it here: Classes final by default, and at the top of that discussion someone links to the book "Effective Java" wherein I guess someone recommends making stuff final by default. I'm not entirely convinced with this as it kinda presupposes all code is written in a specific way: implemented to an interface, and nothing every expects a concrete implementation, basically. I'm OK writing my code like that, however there's no guarantee the author of some lib I need to use also has. And in my experience (granted: in PHP) it's not the case. But so be it. I guess it does encourage one to stop and think. And design to interfaces. Which is a good thing.


Companion objects

I messed with these in a coupla other articles:

I've nothing else I feel I need to check-out on these right now.


Back to backing fields (and backing properties)

Fields

In an earlier article ("Kotlin: there's no such thing as static, apparently"), I learned that a property has a backing field: the underlying variable that stores the value. One just refers to it as field in accessor implementations:

class Person {
    var firstName = ""
        get() = field
        set(value) {
            field = value
        }
    var lastName = ""
        get() = field
        set(value) {
            field = value
        }
}

Under the hood Kotlin is creating appropriately-named discrete private variables for each property's field.

One cool thing IntelliJ can do is to show the bytecode that the Kotlin code would generate (Tools › Kotlin › Show Kotlin Byte Code), and from there one can decompile that back to Java. The result in this case being (I've trimmed some irrelevant stuff):

final class Person {
   private String firstName = "";
   private String lastName = "";

   public final String getFirstName() {
      return this.firstName;
   }

   public final void setFirstName(String value) {
      this.firstName = value;
   }

   public final String getLastName() {
      return this.lastName;
   }

   public final void setLastName(String value) {
      this.lastName = value;
   }
}

One thing I wondered about before when I found myself getting a stack overflow when doing this:

var firstName = ""
    get() = field
    set(value) {
    	// was field = value
        firstName = value
    }

Whether this was because my reference to firstName there was actually calling "itself". It turns out it is, and IntelliJ was even telling me:


Backing properties

This was the next thing that threw me. The docs also talk about backing properties, and really didn't make it at all clear why these existed.

If you want to do something that does not fit into this implicit backing field scheme, you can always fall back to having a backing property

They give an example, and I could understand the code, but it didn't really sink in as to what specifically showing me. I did a google and was pleased to find out I was not the only person a bit flumoxed by all this, and found a good answer here: I have a hard time understanding the purpose of a "backing property". And I was able to contrive a test case to demonstrate it (largely to myself) in action:

class MyList {
    private var _myList = mutableListOf<String>()
    val immutable: List<String>
        get() = _myList
    var mutable: MutableList<String> = mutableListOf()
        set(value) {
            field = value
            _myList = value
        }
}

There's really no trick to it. It's just when an accessor method needs to store its value in some other variable, not its backing field. In the example above I have a class that has a property called mutable that has a setter which will take a MutableList of strings, and another property called immutable which actually accesses the same underlying variable, but returns it as an immutable List. This is a daft example, but it demonstrates the point. I've two properties, and they both access the same underlying data, so it has to be in a specific private property rather than using field which intrinsically is a different variable for each property. Also note I am setting mutable's backing field too. Not for any reason, just to show I can.

Of course I wrote a test for this:

class BackingPropertiesTest : DescribeSpec({
    it("should take a MutableList and return an ImmutableList") {
        val mutableList = mutableListOf("tahi", "rua", "toru")
        val myList = MyList()
        myList.mutable = mutableList

        myList.immutable.shouldBeInstanceOf<List<String>>()
        myList.immutable shouldBe listOf("tahi", "rua", "toru")

        mutableList.add("wha")
        myList.immutable.shouldBeInstanceOf<List<String>>()
        myList.immutable shouldBe listOf("tahi", "rua", "toru", "wha")
    }
})

Straight forward stuff: just demonstrating what the code is supposed to be doing. One thing I thought was neat was how I could append that new element to the mutable list, and when I accessed it again, I got a new immutable list out, also with my new value. It almost seems like I am changing the immutable list, but of course it's a new list each time.


lateinit

This is the solution to a problem that I had to work around the other day. In the end I was "just doing it wrong" for the given situation (I forget what it was now), but I had thought "but I don't want to initalise that with a non-null value now just to satisfy the compiler. I'll deal with it later. Trust me". And of course the "trust me" part is implemented via the lateinit modifier.

class Colour(val en: String) {
    lateinit var mi: String

    fun setMaori(value: String) {
      mi = value
    }
}

Here I have then English version of the colour as a normal property handled by the primary constructor, and another property mi representing the Maori word for the colour, and that isn't handled by any constructor, and is also not initialised. It's happy to stay undefined until "later". If I didn't have the lateinit there, the code would not even compile:

e: [...]\LateInitPropertiesTest.kt: (52, 5): Property must be initialized or be abstract

IntelliJ makes it clear too:

I have a coupla tests demonstrating this in action:

it("allows me to create a colour object without an mi property") {
    val red  = Colour("red")
    red.en shouldBe "red"
}

it("lets me set the mi property later") {
    val orange  = Colour("orange")
    orange.setMaori("karaka")

    orange.mi shouldBe "karaka"
}

What if I try to read that property before it's been initialised?

it("throws UninitializedPropertyAccessException if access mi before it's been set") {
    val yellow  = Colour("yellow")

    val exception = shouldThrow<UninitializedPropertyAccessException> {
        yellow.mi shouldNotBe "kōwhai"
    }
    exception.message shouldBe "lateinit property mi has not been initialized"
}

It throws an exception at runtime. Cool. And it's a specific exception, with a clear message.

For good measure I also checked with a method call with the reference to mi within the class. I did not expect any different behaviour, but I like to check these things.

class Colour(val en: String) {
    lateinit var mi: String

    fun setMaori(value: String) {
      mi = value
    }

    fun getMaori() = mi
}
it("throws UninitializedPropertyAccessException if use mi before it's been set") {
    val green  = Colour("green")

    val exception = shouldThrow<UninitializedPropertyAccessException> {
        green.getMaori() shouldNotBe "kakariki"
    }
    exception.message shouldBe "lateinit property mi has not been initialized"
}

One can also check to see if the property is initialised before using it:

class Colour(val en: String) {
    lateinit var mi: String

    fun setMaori(value: String) {
      mi = value
    }

    fun getMaori() = mi

    fun isMiInitialized() = ::mi.isInitialized
}
it("can have initialisation status checked") {
    val green  = Colour("green")

    green.isMiInitialized() shouldBe false
    green.setMaori("kakariki")
    green.isMiInitialized() shouldBe true
}

I thought I might be able to go green::mi.isInitialized, but I get told off:

Looking in the docs, this is because:

This check is only available for properties that are lexically accessible when declared in the same type, in one of the outer types, or at top level in the same file.

Ooookay. I cannot actually work out how to write code where I can call isInitialized where the property is "declared… in one of the outer types, or at top level in the same file". I always get "Backing field of 'var someValue: String' is not accessible at this point". And that is a coupla hours or so of my life I am never going to get back.


And as that was a bit frustrating at the end there, I'm gonna go and do something else now. You need to picture me with my bottom lip jutted-out in a put-upon way when I say that. I tell you: if I had a ball, I would take it and go home ;-)

Righto.

--
Adam

Tuesday, 20 September 2022

CFWheels: a recommendation for their dev team

 G'day

I'm going to pass this on to the CFWheels Dev Team, but it's applicable in anyone's code, so posting it here first.

A lot of the code in the CFWheels codebase hasn't really been "designed". It's been written as if the developer concerned just went "OK, I have this file open… I'll start typing code in this file…", rather than giving thought to the notions of design patterns or the SOLID principles and stuff like that that exist to keep codebases… usable.

It's legacy code, and we have all written code like this in the past, so let's not dwell too much on how the codebase got to where it is now. Let's just accept that it could be better than it is.

But It's 2022 now, and there's no real excuse for perpetuating coding practices like this.

An example in front of me ATM is the onApplicationStart method. It is 1000 lines long. That is ludicrous.

The design problem in this particular method (and this is the same for a lot of code in the CFWheels codebase) is that it confuses "the things I need to get done", with "how to do those things". It's onApplicationStart's job to… start the application. There are numerous steps to this, but instead of it just calling all the steps it needs to call to define "starting the application", it also has all the implementation of the steps inline in it as well. This is an anti-pattern.

One can even see what each of the steps are. There are comments identifying them. In fact the first two are done "right":

// Abort if called from incorrect file.
$abortInvalidRequest();
// Setup the CFWheels storage struct for the current request.
$initializeRequestScope();

(Except the comments here are pointless as they simply repeat what the method names already say clearly).

But then the wheels (sic) fall off:

if (StructKeyExists(application, "wheels")) {
    // Set or reset all settings but make sure to pass along the reload password between forced reloads with "reload=x".
    if (StructKeyExists(application.wheels, "reloadPassword")) {
        local.oldReloadPassword = application.wheels.reloadPassword;
    }
    // Check old environment for environment switch
    if (StructKeyExists(application.wheels, "allowEnvironmentSwitchViaUrl")) {
        local.allowEnvironmentSwitchViaUrl = application.wheels.allowEnvironmentSwitchViaUrl;
        local.oldEnvironment = application.wheels.environment;
    }
}
application.$wheels = {};
if (StructKeyExists(local, "oldReloadPassword")) {
    application.$wheels.reloadPassword = local.oldReloadPassword;
}

That could should not be inline. There should be a method call inline, called something like configureReloadOptions (or whatever this is doing… a case in point is that I can't really tell).

The thing is onApplicationStart should not be defining the implementation of this, something else should be doing that, and onApplicationStart should just know how to call it.

Why is this an issue?

Because for us, one of the sections of processing in onApplicationStart is wrong, and sufficiently wrong it's been flagged up as a pen vector.

If each part of the application-start-up sequence was in its own functions, then we could override the function with our own implementation to address the issue. And the change would be local to that one small function, so the testing surface area would be small. But instead I now have to hack a third-party codebase to solve this. It's 100s of lines into this method, and accordingly its test surface area is huge (and largely unreachable, I think).

This is basic Open/Closed Principle stuff. The O in SOLID.

My recommendation / rule of thumb is that whenever one is tempted to put a comment in that explains a block of code (or delimit it from other blocks), then that block of code should be in its own function. If one follows this rule of thumb, one seldom gets into the situation that one has Open/Closed Principle issues. It's not foolproof, but it's a good start.

Also having a code design rule that functions should start to raise a flag if they are over 20 lines long. They are probably doing too much, or aren't factored well. If they are more than 50 lines? Stop. Recode them.

If a function has unrelated flow-control blocks in them? Also a red flag. Each block is likely more appropriate to be in their own functions.

A function like onApplicationStart that needs to carry-out a _lot_ of steps to consider the app to be "started" should look something like this:

function onApplicationStart() {
    doTheThing()
    doAnotherThing()
    doSomethingElse()
    doThisThingRelatingToStuff()
    doOtherStuff()
    doThis()
    doThat()
   // ...
}

It really shouldn't be implementing any stuff or things or this or that itself.

Lastly, someone drew my attention to this statement on the CFWheels home page:

Good Organization

Stop thinking about how to organize your code and deal with your business specific problems instead.

This possibly explains a lot of the way the CFWheels codebase got to be the way it is. It is really bad advice. One should always be thinking about how one's application ought to be designed. It's important.

NB: this is not just related to onApplicationStart. That's just what currently has me sunk. It relates to really a lot of the code in the codebase.

This is also something that can be tackled in a piecemeal fashion. If one has to maintain some code in a really long method: extract the block of code in question into its own function, and maintain (and test that).

Righto.

--
Adam

Monday, 19 September 2022

Kotlin: investigating classes

G'day:

In my other random explorations of the Kotlin language, I've already used simple classes to facilitate testing other language features, but my next koans exercise (man I am progressing slowly with those!) links to the docs page for classes, so I'm gonna have a breeze through that, so I can extend my understanding of how they operate beyond the superficial level I currently have. Anything that intrigues me or is non-obvious, I'll write a test and sling it in here.


Primary and secondary constructors

I touched on this in "Kotlin: another Friday afternoon, another round of random investigation › Primary and secondary constructors", but only in as much as I messed up my syntax for my constructor and IntelliJ fixed it for me.

Primary constructor

This is baked-into the class declaration:

class Suffragist(firstName:String, lastName:String) {
    val fullName = "$firstName $lastName"
}

class ClassesTest : DescribeSpec({
    describe("Constructor tests") {
        it("takes values that can be used in initialisation code") {
            val suffragist = Suffragist("Kate", "Sheppard")

            suffragist.fullName shouldBe "Kate Sheppard"
        }
    }
})

It's important to understand that the primary constructor here is just the bit in parentheses. The braces are not the constructor implementation, that is the class implementation, for example in Java that constructor (in context) would be:

class Suffragist {

    String firstName;
    String lastName;

    public Suffragist(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

So the primary constructor identifies the properties needed for initialisation of the object.

In my class above, firstName and lastName are not properties of the Suffragist object. In my test I could not do this:

suffragist.firstName shouldBe "Kate"
suffragist.lastName shouldBe "Sheppard"

This gives a compile error:

Unresolved reference: firstName

To make them properties, I need to put a val or var qualifier in the primary constructor:

class Scientist(val firstName:String, val lastName:String)

class ClassesTest : DescribeSpec({
    describe("Constructor tests") {
        // ...
        it("needs to qualify params to make them properties") {
            val scientist = Scientist("Siouxsie", "Wiles")

            scientist.firstName shouldBe "Siouxsie"
            scientist.lastName shouldBe "Wiles"
        }
    }
})

Back to the first example:

class Suffragist(firstName:String, lastName:String) {
    val fullName = "$firstName $lastName"
}

That line is not part of the constructor, it's initialising the fullName property. It just happens to have access to the firstName and lastName values from the constructor when it's executed.


Initializer blocks

As the primary constructor can't have any code in it, any code that needs to run when the object is created needs to be in initializer blocks in the class body:

class Politician(firstName:String, lastName:String) {
    init {
        println("First name: $firstName")
    }
    init {
        println("Last name: $lastName")
    }
}

class ClassesTest : DescribeSpec({
    describe("Constructor tests") {
        
        // ...
        
        it("can have code in initializer blocks") {
            val EOL = System.lineSeparator()
            val output = SystemLambda.tapSystemOut {
                Politician("Jacinda", "Ardern")
            }

            output shouldBe "First name: Jacinda${EOL}Last name: Ardern${EOL}"
        }
    }
})

Unsurprisingly they are executed in the order they are in the file.


Secondary constructors

Kotlin can have secondary constructors if one needs to do more than take some arguments and pass them to initializer blocks. Secondary constructors use the constructor keyword to define their behaviour.

Here I have a secondary constructor that takes a map of firstName / lastName pairs, and this extracts the values from the map for each of the firstName / lastName parameters of the primary constructor.

class FilmMaker(firstName:String, lastName:String) {
    val fullName = "$firstName $lastName"
    constructor(names: Map<String, String>) : this(names["firstName"]!!, names["lastName"]!!)
}

class ClassesTest : DescribeSpec({
    describe("Constructor tests") {

        // ...
        
        it("can have a secondary constructor") {
            val names = mapOf(Pair("firstName", "Jane"), "lastName" to "Campion")
            val filmMaker = FilmMaker(names)

            filmMaker.fullName shouldBe "Jane Campion"
        }
    }
})

It's important to note that because I have a primary constructor defined, then ultimately one of the secondary constructors must call it, even if there's a sequence of secondary constructors calling one another in a chain, ultimately the primary constructor must be called.

I did wonder why it's not possible to do this sort of thing:

constructor(names: Map<String, String>) {
    val firstName = names["firstName"]
    val lastName = names["lastName"]

    this(firstName!!, lastName!!)
}

IE: to have the secondary constructor do some work and then call the primary constructor. I could not find an answer for this.

BTW, that !! operator is a not-null assertion operator. In this usage it tells the compiler to error-out if the map doesn't have the key/value pair I'm using there. Otherwise I'd need to make the following code handle nulls for those values, and that is incorrect.


Here's an example where there's no primary constructor, which enables a secondary constructor to do more than simply invoke the primary one, inline:

class ChiefJustice {
    var firstName = ""
    var lastName = ""
    var fullName = ""

    constructor(firstName:String, lastName:String) {
        this.firstName = firstName
        this.lastName = lastName

        fullName = "$firstName $lastName"
    }

    constructor(names: List<String>) : this(names.first(), names.last())
}

class ClassesTest : DescribeSpec({
    describe("Constructor tests") {

        // ...

        it("can call another secondary constructor") {
            val names = listOf("Helen", "Winkelmann")
            val chiefJustice = ChiefJustice(names)

            chiefJustice.fullName shouldBe "Helen Winkelmann"
        }
    }
})

Here we have two secondary constructors. One takes separate String values for the properties, and the second takes a list of names, and extracts first and second to pass to the previous secondary constructor to actually then do the initialisation work.


Companion objects and factory methods

I touched on companion objects before in my "Kotlin: there's no such thing as static, apparently" article. One thing I read a few times when reading about secondary constructors, is that they're a bit frowned-up for situations where there's constructors taking different repesentations of the property data values (like all the examples I offer here!). Instead of having a constructor that takes a map of name/value pairs, one would instead have a factory method, using a companion object. So instead of this example:

class FilmMaker(val firstName:String, val lastName:String) {
    val fullName = "$firstName $lastName"
    constructor(names: Map<String, String>) : this(names["firstName"]!!, names["lastName"]!!)
}

One might better have:

class FilmMaker(firstName:String, lastName:String) {
    val fullName = "$firstName $lastName"

    companion object Factory {
        fun createFromMap(names: Map<String, String>) = FilmMaker(names["firstName"]!!, names["lastName"]!!)
    }
}

class ClassesTest : DescribeSpec({
    describe("Constructor tests") {
        it("can have a secondary constructor") {
            val names = mapOf(Pair("firstName", "Jane"), "lastName" to "Campion")
            val filmMaker = FilmMaker.createFromMap(names)

            filmMaker.fullName shouldBe "Jane Campion"
        }
    }
})

Now I have a factory method createFromMap which is a bit more clear in its intent / purpose than having multiple vague constructors.


I'm gonna leave it there today. There's some more sections on that docs page regarding inheritance and abstract classes and stuff, but I'm not in the mood for that right now. Am struggling a bit with focus just now: not quite sure why.

The code for today is on GitHub as ClassesTest.kt (1.5 tag) except the last example which was a refactor of the earlier code, and is tagged as 1.5.1: ClassesTest.kt.

Righto.

--
Adam