Search code examples
kotest

How to use output from one generator in another generator in Kotest?


Using an example from Clojure's test.check let generator, generate a non-empty list of strings, give that list to another generator to pick a string from, then create a map that contains the string list and the selected string. In Clojure, it looks as follows:

(gen/let [list-of-strings (gen/not-empty (gen/list gen/string))
          a-string        (gen/element list-of-strings)]   ;; use the generated list-of-strings above
  {:all-strings list-of-strings
   :selected    a-string})

Taking io.kotest.property.arbitrary.bind for inspiration, I've tried implementing it as follows, but it doesn't work (Kotlin compiler spitted out "Type inference failed"):

fun <A, B, T: Any> let(genA: Gen<A>, genB: (A) -> Gen<B>, bindFn: (A, B) -> T): Arb<T> {
    return arb { rs ->
        val iterA = genA.generate(rs).iterator()

        generateSequence {
            val a = iterA.next()
            val iterB = genB(a.value).generate(rs).iterator()
            val b = iterB.next()
            bindFn(a.value, b.value)
        }
    }
}

Solution

  • Turns out dropping bindFn parameter solves the problem, but the solution looks a little ugly as it needs to return a Pair:

    fun <A, B> let(genA: Gen<A>, genBFn: (A) -> Gen<B>): Arb<Pair<A, B>> {
        return arb { rs ->
            val iterA = genA.generate(rs).iterator()
    
            generateSequence {
                val a = iterA.next().value
    
                // could combine the following to one line, but split for clarity
                val genB = genBFn(a)
                val iterB = genB.generate(rs).iterator()
                Pair(a, iterB.next().value)
            }
        }
    }
    

    Then with the above, using it looks as follows:

    class StringTest : StringSpec({
        "element is in list" {
            val letGen = let(
                Arb.list(Arb.string(), range=1..100),    // genA
                { xs -> Arb.element(xs) }                // genBFn
            )
    
            forAll(letGen) { (xs, x) ->
                x in xs
            }
        }
    })