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