(Also available in JavaScript and Kotlin flavours)
Databob provides a way to generate completely randomised object builders with zero-boilerplate code.
The problem of generating dummy test instances for our classes has been around for a long time. Given the following case classes...
case class ReadReceipt(value: Boolean)
case class EmailAddress(value: String)
case class Email(from: EmailAddress, to: Seq[EmailAddress], date: ZonedDateTime, read: Boolean, subject: String, readReceipt: Try[ReadReceipt])
case class Inbox(address: EmailAddress, emails: Seq[Email])
We could start to write objects using the TestBuilder pattern using the traditional method:
class InboxAddressBuilder {
private var address = EmailAddress("[email protected]")
private var emails = Seq[Email]()
def withAddress(newAddress: EmailAddress) = {
address = newAddress
this
}
def withEmails(newEmails: Seq[Email]) = {
emails = newEmails
this
}
def build = Inbox(address, emails)
}
Scala makes this easier for us somewhat by leveraging Case class copy()
. This also allows us to be compiler safe, as removing
a field will break the equivalent with
method:
class InboxAddressBuilder {
private var inbox = Inbox(EmailAddress("[email protected]"), Seq[Email]())
def withAddress(newAddress: EmailAddress) = {
inbox = inbox.copy(address = newAddress)
this
}
def withEmails(newEmails: Seq[Email]) = {
inbox = inbox.copy(emails = newEmails)
this
}
def build = inbox
}
Taking this even further with default arguments, we can reduce this to:
object InboxAddressBuilder {
def apply(address: EmailAddress = EmailAddress("[email protected]"),
emails: Seq[Email] = Nil)
= Inbox(address, emails)
}
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.
No sharing of test data across tests. Ever.
For a completely randomised instance, including non-primitive sub-tree objects:
Databob.random[Email]
That's it. Want to override particular value(s)?
Databob.random[Inbox].copy(address = EmailAddress("[email protected]"))
Or add your own rule for generating values?
implicit val generators = typeIs(databob => EmailAddress(databob.mk[String] + "@" + databob.mk[String] + ".com")) +: Generators.EmptyGenerators
Databob.random[Email]
Additionally, once we have this randomising structure in code, we can use it for other useful stuff. For example, generating JSON with a library such as Json4S:
case class Book(title: String, pages: Int)
case class Teacher(firstName: String, lastName: String)
case class SchoolLibrary(librarian: Teacher, books: Seq[Book])
implicit val f = DefaultFormats
pretty(render(decompose(Databob.random[SchoolLibrary])))
... would generate us something like this:
{
"librarian":{
"firstName":"6c550709-bdc8-4ce8-8acd-607020f027bb",
"lastName":"11073325-20fb-4d81-832c-d2eacd5bc4f1"
},
"books":[{
"title":"982c7e30-a969-40f1-99c1-f397d1c52494",
"pages":713182742
}]
}
Or to get XML...
<SchoolLibrary>
{Xml.toXml(decompose(Databob.random[SchoolLibrary]))}
</SchoolLibrary>
...producing this:
<SchoolLibrary>
<librarian>
<firstName>e1981fac-f3f4-4abf-82e4-374975d2b585</firstName>
<lastName>75a13eca-1ee0-4ec0-aff4-1ab3026b5acf</lastName>
</librarian>
<books>
<title>50029977-7566-43d5-83fa-09affdcbd7d5</title>
<pages>1502236860</pages>
</books>
<books>
<title>d25a697c-8960-4d9b-b595-224ac07df78a</title>
<pages>1777810872</pages>
</books>
</SchoolLibrary>
- Nested object-trees (ie. non-primitive fields)
- All Scala/Java primitives: Default, random
- Scala and Java Collection classes: Empty, single-value, variable size, random
- Java8 date-time values: Epoch, current-time, random
- Some monadic types (Option/Either/Try/Future): Happy, unhappy, random
- Simple overriding mechanism for your own-types and custom generation rules
See the example code.
Add the following lines to build.sbt
:
resolvers += "JCenter" at "https://jcenter.bintray.com"
libraryDependencies += "io.github.daviddenton" %% "databob" % "1.5.0"
PRs gratefully accepted for other common types that might be useful.
To Json4S for the ninja reflection util.