3

I want to be able to generate an exhaustive permutation of objects. Imagine the following object

data class Person (name: String, age: Int)

For testing purposes, I want to restrict the name to 3 values. Mohammad, Nasir, Rasul and age to 4 values. 10, 20, 30, 40. I want to generate 12 objects, where each name has each of the 4 ages.

I can generate an arbitrary binding, however that doesn't gurantee 12 iterations will each have a unique object. I have to increase the iteration count, and weed out duplicates.

    val list = Arb.bind(
        listOf("Nasir", "Rasul", "Mohammad").exhaustive(),
        listOf(10, 20, 30, 40).exhaustive()
    ) { name, age -> Person(name, age) }

    "Test person " - {
        runBlocking {
            list.checkAll(12) {
                System.out.println("Testing $it")
                assertTrue(it.age < 50)
            }
        }
    }

Looking in the source code, I can't seem to find a way. I am hoping someone in the community has had a need for this.

Thanks.

Note: I am looking for a way with Exhaustive generator, not the Arb generator. I could do some post processing and remove duplicates, but I am hoping for something more reliably unique upfront.

Example outout:

Testing Person(name=Mohammad, age=40)
Testing Person(name=Nasir, age=20)
Testing Person(name=Rasul, age=20)
Testing Person(name=Rasul, age=30)
Testing Person(name=Mohammad, age=20)
Testing Person(name=Rasul, age=40)
Testing Person(name=Nasir, age=10)
Testing Person(name=Rasul, age=10)
Testing Person(name=Nasir, age=40)
Testing Person(name=Rasul, age=40)
Testing Person(name=Nasir, age=30)
Testing Person(name=Mohammad, age=30)

Notice the Rasul:40 is duplicated. Mohammad:10 missed out.

1 possible solution based on @Tenfour04's comment is to use times and map. Though with my fields, the mapping becomes hairy, as we'll have Pairs and Pairs and Pairs to process.

    "Test cross product" - {
        val times = Exhaustive.collection(listOf("Nasir", "Rasul"))
            .times(Exhaustive.collection(listOf(10, 20)))
            .map { Person(it.first as String, it.second) }
        runBlocking {
            times.checkAll(4) {
                println("$it")
            }
        }
    }
Nasir
  • 2,984
  • 30
  • 34
  • This answer your question? https://stackoverflow.com/questions/63064837/is-there-a-simpler-way-to-test-all-permutations-with-kotest-property-based-testi – Tenfour04 Feb 21 '21 at 04:25
  • Thanks @Tenfour04. Not quite. I want to be able to define this in the context of object, and the linked construct seems outside. I'll give it a try though, but at first glance I don't think it will work. I'll report back, if I find results to the contrary. – Nasir Feb 21 '21 at 05:15
  • I think it'll work. I just needed to `map` it to an object.. Thanks @Tenfour04. With more than 2 properties though, it starts becoming convoluted due to use of `Pair`. – Nasir Feb 21 '21 at 05:23
  • 1
    Wy not just generate all possible combinations manually `val persons = names.flatMap { name -> ages.map { Person(name, it) } }`? – Михаил Нафталь Feb 21 '21 at 12:05
  • @МихаилНафталь I am not certain I follow. I will have a data structure which will have a different set of values for each attribute, some enums, some other finite set ... and if I understand you correctly, your suggestion would work if it was only two things ... – Nasir Feb 21 '21 at 22:15

1 Answers1

4

You can do this by just mapping over each of the values in each component and combining them into a new Exhaustive. For example, if you have three components you want to generate all the combinations for:

fun <A, B, C, D> cartesian(
   a: Exhaustive<A>,
   b: Exhaustive<B>,
   c: Exhaustive<C>,
   f: (A, B, C) -> D
): Exhaustive<D> {
   val ds = a.values.flatMap { _a ->
      b.values.flatMap { _b ->
         c.values.map { _c ->
            f(_a, _b, _c)
         }
      }
   }
   return ds.exhaustive()
}

Then that exhaustive can be used in a test (And so on for arity-2 etc).

Here is how you would use it for your persons example.

val persons = cartesian(
   Exhaustive.collection(listOf("Nasir", "Rasul")),
   Exhaustive.collection(listOf(10, 20))
) { a, b -> Person(a, b) }

checkAll(persons) { person -> .... test here .... }

Note: The above function exists in Kotest 4.5 which at time of writing hasn't been released. https://github.com/kotest/kotest/blob/master/kotest-property/src/commonMain/kotlin/io/kotest/property/exhaustive/cartesian.kt

sksamuel
  • 16,154
  • 8
  • 60
  • 108