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
473 stars 44 forks source link

TypeGenerator-s are not applied for elements in collections #242

Closed tomas0svk closed 4 months ago

tomas0svk commented 5 months ago

If I specify a type generator for given type and use the type as a generic (e.g. in a collection), typeGenerator is not applied.

MWE:

import io.github.serpro69.kfaker.Faker
import io.github.serpro69.kfaker.fakerConfig
import org.junit.jupiter.api.Test

class DeleteMe2 {
    val faker = Faker(
        fakerConfig {
            randomClassInstance {
                typeGenerator<Int> { 42 }
            }
        }
    )

    data class Foo(
        val asField: Int,
        val inList: List<Int>,
        val inMap: Map<Int, Int>,
    )

    @Test
    fun testFoo() {
        // Foo(asField=42, inList=[100051301], inMap={-773451690=1221496510})
        println(faker.randomProvider.randomClassInstance<Foo>())
    }
}

I would expect the same behavior as any constructor invocation

import io.github.serpro69.kfaker.Faker
import io.github.serpro69.kfaker.fakerConfig
import org.junit.jupiter.api.Test
import java.math.BigDecimal

class DeleteMe {
    val faker = Faker(
        fakerConfig {
            randomClassInstance {
                // Like this I face an error Failed to instantiate class java.math.BigDecimal with [EXlVdxQfbdqS2JBpvDcCKq9S]
                // Tries to call constructor BigDecimal(String) with a randomly generated string, which fails.
                typeGenerator<BigDecimal> { BigDecimal.ONE }

                // But if I create typeGenerator for String (returning only numeric chars), it works OK
                // Foo(asField=1, inList=[123456], inMap={-4605729964128144721=123456})
                typeGenerator<String> { "123456" }
            }
        }
    )

    data class Foo(
        val asField: BigDecimal,
        val inList: List<BigDecimal>,
        val inMap: Map<Long, BigDecimal>
    )

    @Test
    fun testFoo() {
        println(faker.randomProvider.randomClassInstance<Foo>())
    }
}
serpro69 commented 5 months ago

Hi @tomas0svk :wave: ,

Custom generators like typeGenerator or namedParameterGenerator are applied to parameters of the constructor. When you set typeGenerator<String> that will be applied to all parameters of String type, in a constructor of every class that you're trying to generate (including all dependencies in complex types) By default the first encountered constructor with least number of arguments is used, which in your case I suppose results in a constructor that takes an input as string. That is why setting typeGenerator<String> { "123456" } returns a valid BigDecimal instance; while not setting explicitly how strings are generated, will result in a random string passed to all constructor parameters with String type.

Creating collections is a bit different. We just figure out collection type (List, Set, Map) and the element type (e.g. BigDecimal) and generate a given number of random elements of that collection using the above same logic for generating any random instance. typeGenerator<BigDecimal> or any other type generator function doesn't apply in this case, because BigDecimal is not a parameter input for the collection constructor.

I think one way to support this would be to introduce some kind of collectionTypeGenerator function , that would allow to configure how elements in a collection are generated.

While I take a look into how to implement this (unless you'd like to do a PR yourself?), depending on your use-case, maybe namedParameterGenerator would solve your current problem? E.g. you could do:

namedParameterGenerator("inList") {
    List(10) {
        BigDecimal(faker.random.nextInt(1..1000))
    }
}

This would generate Foo class with inList parameter having a list of 10 random BigDecimal values in a given range.

Of course this depends on how many classes you have that need this kind of customization.

serpro69 commented 4 months ago

This is now available in latest snapshot version, and I'll soon publish the next release candidate ( v2.0.0-rc.5 ) with this change as well. Usage details are described here https://serpro69.github.io/kotlin-faker/wiki/extras/#predefined-collection-element-types Thanks again for opening this issue, @tomas0svk .