serpro69 / kotlin-faker

Port of a popular ruby faker gem written in kotlin. Generate realistically looking fake data such as names, addresses, banking details, and many more, that can be used for testing and data anonymization purposes.
https://serpro69.github.io/kotlin-faker/
MIT License
453 stars 42 forks source link

Allow RandomClassProvider::randomClassInstance to use predefinedGenerators from config if present #201

Closed artdk closed 8 months ago

artdk commented 8 months ago

When generating a random object of type T via the randomProvider.randomClassInstance<T>() method, if a typeGenerator is defined for T then it would be helpful if randomClassInstance simply used that generator instead of ignoring it

Example

I want to generate a random Instant (which does not have a public constructor) using a configured typeGenerator:

val f = Faker()
f.randomProvider.configure {
    typeGenerator<Instant> { Instant.now() }
}

In this case f.randomProvider.randomClassInstance<Instant>() throws java.util.NoSuchElementException: No suitable constructor found for class java.time.Instant because the implementation of randomClassInstance<Instant>() only calls KClass<T>.predefinedTypeOrNull for the types of constructor args (of which there are none in the case of Instant)

This is not an issue when the the type of the predefined generator is not the type specified in randomClassInstance<Instant>(), for example if I define data class Foo(val instant: Instant), then f.randomProvider.randomClassInstance<Foo>() generates correctly

Possible solution

I propose adding a line to the beginning of RandomClassProvider's KClass<T>.randomClassInstance(config: RandomProviderConfig) method that attempts to get a random instance from the predefined generators, and then return if the predefined generators return something. I am not sure how this would be best implemented, as the generators are designed to work with KParameters. One kinda hacky idea is to define something like:

fun KClass<*>.toParameterInfo() = ParameterInfo(
    index = 0,
    name = jvmName,
    isOptional = false,
    isVararg = false,
)

which would allow val predefinedInstance = predefinedTypeOrNull(config, toParameterInfo()) as? T? above line 105 of RandomClassProvider.kt and then the return statement on line 117 could become return predefinedInstance ?: defaultInstance ?: objectInstance ?: run {

Is this a change you would be interested in? And if so you have any suggestions on how best it could be implemented?

serpro69 commented 8 months ago

Hi @artdk ,

I'm always interested in something that brings value :) but would like to first understand your use-case. Do you actually want to generate an instance of Instant via randomClassInstance function? Or is instant a property in another class?

Because if it's the former, then it's not really going to be "random instance" of Instant as such, it will always return Instant.now() (or whatever you configure via typeGenerator). So why not just call Instant.now() wherever you need an instance of it, instead of faker.randomClassInstance<Instant>()? The result will be the same.

PS: I do get that you're using instant as an example here, but I'm not really seeing the use-case. So maybe you could elaborate on what you're trying to achieve.

serpro69 commented 8 months ago

This change is now in master and should be available in latest snapshot version after the build completes. I want to give it a bit more thought before getting this into an RC version, but feel free to try out the snapshot and share your feedback, if you have any. Thanks for suggesting this feature :)

artdk commented 8 months ago

Sorry for the delay, The most pressing use case is when testing generic functions. For example we have an inline fun <reified T: Any> myFun<T>(p0: T): Boolean which contains logic using the values of member variables which may or may not exist in p0; if we define this highly simplified test

@Test
fun myTest() {
      testClass(Foo::class, true)
      testClass(Instant::class, true)
      testClass(Bar::class, false)
      ...
}

private inline fun <reified T: Any> testClass(klass: KClass<T>, expectedMyFunValue: Boolean) {
    val testVal = faker.randomProvider.randomClassInstance<T>()
    myFun(testVal) shouldBe expectedMyFunValue
}

This allows us to test myFun against a number of different types, but if one of those types has no public constructors then it cannot be tested using this generic method and instead we must provide an alternative generator for those specific cases

The other benefit here is consistency and control; for example in a given domain we have constraints on Instant, so we introduce a function randomConstrainedInstant() to generate Instants that fit these constraints; if we then want to test a function myFun(p0: Foo, p1: Instant, p2: Foo): Bar we can define the test variables as:

val p0 = faker.randomClassInstance<Foo>()
val p1 = randomConstrainedInstant()
val p2 = randomConstrainedInstant()
val result = myFun(p0, p1, p2) 
// assert on result comparing to p0, p1, and p2

This is much more consistent if we can just use faker.randomClassInstance<Instant>() for p1 and p2, and doesnt require that every developer has to remember all the cases where a a random object cannot be generated and all the exceptional random object generation functions that fill those gaps

I hope this helps elaborate on some of the motivation and cases that inspired this, and thanks for being so receptive to such a corner-case feature!

serpro69 commented 8 months ago

Thanks for the examples. I think this is a pretty valid use-case.