Databob.kotlin

Randomised, zero-boilerplate object builders

This project is maintained by daviddenton

databob.kotlin

coverage kotlin build status bintray version

(Also available in Scala and JavaScript flavours)

Databob provides a way to generate completely randomised objects with zero-boilerplate builder code.

Why?

The problem of generating dummy test instances for our classes has been around for a long time. Given the following data classes...

data class ReadReceipt(val read: Boolean)

data class EmailAddress(val value: String)

data class Email(val from: EmailAddress, val to: List<EmailAddress>, val date: ZonedDateTime, val read: Boolean, val subject: String, val readReceipt: Option<ReadReceipt>)

data class Inbox(val address: EmailAddress, val emails: List<Email>)

We could start to write objects using the TestBuilder pattern using the traditional method:

class InboxBuilder {
    private var address = EmailAddress("[email protected]")
    private var emails = listOf<Email>()

    fun withAddress(newAddress: EmailAddress): InboxBuilder {
        address = newAddress
        return this
    }

    fun withEmails(newEmails: List<Email>): InboxBuilder {
        emails = newEmails
        return this
    }

    fun build() = Inbox(address, emails)
}

Kotlin makes this easier for us somewhat by leveraging data class copy(). This also allows us to be compiler safe, as removing a field will break the equivalent with method:

class BetterInboxBuilder {
    private var inbox = Inbox(EmailAddress("[email protected]"), listOf<Email>())

    fun withAddress(newAddress: EmailAddress) : BetterInboxBuilder {
        inbox = inbox.copy(address = newAddress)
        return this
    }

    fun withEmails(newEmails: List<Email>): BetterInboxBuilder {
        inbox = inbox.copy(emails = newEmails)
        return this
    }

    fun build() = inbox
}

Taking this even further with immutability and using data class copy(), we can reduce this to:

class EvenBetterInboxBuilder private constructor(private val inbox: Inbox) {

    constructor() : this(Inbox(EmailAddress("[email protected]"), listOf<Email>()))

    fun withAddress(newAddress: EmailAddress) = EvenBetterInboxBuilder(inbox.copy(address = newAddress))

    fun withEmails(newEmails: List<Email>) = EvenBetterInboxBuilder(inbox.copy(emails = newEmails))

    fun build() = inbox
}

So, better - but it still seems pretty tedious to maintain. Additionally, we don't really want tests to rely unknowingly on bits of default test data for multiple tests which will lead to an explosion of ObjectMother-type methods with small variations to suit particular tests.

What we really want are completely randomised instances, with important overrides set-up only for tests that rely on them.

No sharing of test data across tests. Ever.

Enter Databob

For a completely randomised instance, including non-primitive sub-tree objects:

Databob().mk<Email>()

That's it. Want to override particular value(s)?

Databob().mk<Email>().copy(from = EmailAddress("[email protected]"))

Or add your own rule for generating values?

val override = Generators.ofType { databob -> EmailAddress(databob.mk<String>() + "@" + databob.mk<String>() + ".com") }

Databob(override).mk<Email>()

Out-of-the-box features:

See it in action

See the example code.

Get it

<dependency>
  <groupId>io.github.daviddenton</groupId>
  <artifactId>databob.kotlin</artifactId>
  <version>X.X.X</version>
</dependency>

For extension binding support (such as Funktionale), you'll need to also add relevant dependencies)

Contribute

PRs gratefully accepted for generators for other common Library types that might be useful. Note the idea is for the library to NOT depend on any other libraries, but to simply ship the bindings to plug them in.