KFactory
Create best-in-class factories for your synthetic data in Kotlin.
About KFactory
Why synthetic data?
Chances are that synthetic data are going to be a major painpoint for your project sooner than later.
- Local development
- Unit tests
- Exploratory testing
- Showcase a feature
- Pre-production environments
Are you going to need synthetic data for any of the above?
Yes!
- Probably for all of them
Synthetic data strategies
There are 3 major ways to create synthetic data that we are aware of:
- Static fixtures
- Factories
- Production copies
Some other times, production copies can be too small - during the early days of a product - or too large to be of practical use. And certainly, you cannot really write unit or functional tests on production copies cause they are dynamic and unpredictable. We typically recommend production copies for populating pre-production environments.
Why KFactory?
Other ecosystems have robust synthetic data solutions for some time now, mostly inspired from Ruby's amazing FactoryBot.
KFactory is also inspired by FactoryBot - in a Kotlin idiomatic way.
- Built-in helpers
- Composable factories
- Traits
- Lazy sequence builds
Installation
KFactory is published on mavenCentral
. In order to use it just add the following dependency:
implementation("io.github.bluegroundltd:kfactory:1.0.0")
Usage
You can find a lot of factory examples inside examples directory!
API Reference
API reference is available under this link!
Creating a new Factory
When you have a domain entity that you want to create a Factory
for you can start by doing the following:
class AddressFactory : Factory<Address> {
override fun produce() : Address = Address()
}
Introducing Factory traits
A lot of times we want to produce fixtures from factories, but we only need to change only a few of their attributes/characteristics.
For example:
class AddressFactory(
private var city: String = "city",
private var state: String = "state",
) : Factory<Address> {
fun withCity(city: String) = apply {
this.city = city
}
fun withState(state: String) = apply {
this.state = state
}
override fun produce() : Address = Address(
city = city,
state = state
)
}
Now if we want to produce several instances of Address
that will retain city
but will have another specific value for state
, we can create a new FactoryTrait
that we will later apply to that Factory
.
object CaliforniaTrait : FactoryTrait<AddressFactory> {
override fun modifyWithTrait(factory: AddressFactory): AddressFactory = factory
.withState(state = "California")
}
Enhancing a Factory with Traits
In order to enhance our Factory
with a FactoryTrait
like we previously saw, we need to use the TraitEnhancedFactory
marker interface.
For example consider the following:
class AddressFactory(
private var city: String = "city",
private var state: String = "state",
) : Factory<Address>, TraitEnhancedFactory {
fun withCity(city: String) = apply {
this.city = city
}
fun withState(state: String) = apply {
this.state = state
}
override fun produce() : Address = Address(
city = city,
state = state
)
}
This immediately adds two new extension functions on our Factory
:
fun withTraits(vararg traits: FactoryTrait)
fun withTrait(trait: FactoryTrait)
We can now start building factories with distinctive characteristics:
val californiaFactory: AddressFactory = AddressFactory()
.withTraits(CaliforniaTrait)
Producing objects from a Factory
As described above we utilize factories in order to produce fixture data.
This can be done by invoking the following function on a Factory
:
val address: Address = californiaFactory.produce()
If we need to generate more than on instance of our fixture data, we can utilize the following function of a Factory
that returns a Sequence
of objects:
val addresses: List<Address> = californiaFactory.produceMany()
.take(5)
.toList()
Generating dynamic values every time
Most of the time in our fixture data we might need to produce random values, or have a new value generated every time we invoke .produce()
on one of our factories.
For that purpose, we include a typealias
in our library, named Yielded
and our proposed usage is the following:
class AddressFactory(
private var city: String = "city",
private var state: String = "state",
private var streetNum: Yielded<Int> = { Random.nextint(1,5) }
) : Factory<Address>, TraitEnhancedFactory {
fun withCity(city: String) = apply {
this.city = city
}
fun withState(state: String) = apply {
this.state = state
}
fun withStreetNum(streetNum: Int) = apply {
this.streetNum = { streetNum }
}
fun withStreetNum(streetNum: Yielded<Int>) = apply {
this.streetNum = streetNum
}
override fun produce() : Address = Address(
city = city,
state = state,
streetNum = streetNum()
)
}
From the above example, we can see that we have two new functions in our Factory
.
These functions allow us to override the value generated for streetNum
to have either a static value every time we invoke .produce()
, or a dynamic one. By default, the value of it will be a lambda function which delegates to Random.nextInt()
each time.
Publishing
- Bump version in
gradle.properties
ofkfactory
module. - Execute the following to upload artifact:
$ ./gradlew :kfactory:publish \
--no-daemon --no-parallel \
-Psigning.secretKeyRingFile=<keyring_file_path> \
-Psigning.password=<keyring_password> \
-Psigning.keyId=<keyring_id> \
-PmavenCentralUsername=<nexus_username> \
-PmavenCentralPassword=<nexus_password>
After this operation finishes, you can promote the artifact to be releasable with:
$ ./gradlew closeAndReleaseRepository \
-PmavenCentralUsername=<nexus_username> \
-PmavenCentralPassword=<nexus_password>
Maintainers
The core maintainer of this project, is the Platform Team of Blueground!