abendt
03/18/2023, 11:04 AMMap<A,B>
for input.
Initially i specified the maps like this:
checkAll(Arb.map(Arb.long(), Arb.boolean()), Arb.map(Arb.long(), Arb.boolean())) { a, b ->
…
}
When checking the test coverage i found that some of the code branches were never executed. In the code there are specific cases when a specific key is present only in the left, right or in both maps. I learned that the above code usually does not generate maps that share any keys.
So I tried some ways to improve that. What I have come up with so far is a specialized Arb with the following signature:
fun <K, A, B> Arb.Companion.map2(
genK: Gen<K>,
genA: Gen<A>,
genB: Gen<B>,
size: IntRange = 0..1000,
slippage: Int = 10,
shared: IntRange = 0..100
): Arb<Maps2Result<K, A, B>> = ...
It generates two maps, You can specify the size range for both maps and also specify the percentage of keys both maps should share.
checkAll(Arb.map2(Arb.long(), Arb.boolean(), Arb.boolean())) { (a, b) ->
…
}
I would be curious to get some feedback on the approach? Also, is this something that could be of general use and could be added to the kotest codebase?
Also wondering what alternative ways/patterns are there for the problem?Emil Kantis
03/18/2023, 9:29 PMfun <T, A, B> Arb<T>.flatMap(fn: (T) -> Pair<Arb<A>, Arb<B>>): Pair<Arb<A>, Arb<B>>
/**
* Returns a generator which mutates this map. New values of [T] are generated using the [ts] arb.
* Edge cases: emtpy map, original map without modifications
*/
fun Map<T>.mutating(ts: Arb<T>): Arb<Map<T>>
Then you can compose these two functions to fulfill this need, but it could also be used in many more situations where you want two arbs derived from a single one. What do you think?Emil Kantis
03/18/2023, 9:29 PMabendt
03/19/2023, 10:41 AMabendt
03/19/2023, 10:43 AMmitch
03/19/2023, 11:25 AMArb.map3
all the way to Arb.mapN
? Curious if that’s the case we might need to think about how might we make sure we can make sure maintenance work of these arbs and the work to otherwise extend it is not too overwhelming for the maintainers?mitch
03/19/2023, 11:35 AMArb<Map<K, Value2<A, B>>
. Here you can imagine that Value2 is a sealed class being First<A>
Second<B>
or Both<A, B>
mitch
03/19/2023, 11:35 AMmitch
03/19/2023, 11:39 AMfun <K, A, B > Map<K, Value2<A, B>>.split(): Pair<Map<K, A>, Map<K, B>> = ...
mitch
03/19/2023, 11:43 AMArb.map(arbKey, arbValue2)
you can then destructure that as appropriate in the test. This means that you won't really need to add an additional operations in kotest, but rather leverage what's already given in the current framework. You also have full control on how the value arb is generated as they seem to be very tightly coupled with the domain.mitch
03/19/2023, 12:19 PMOption<A>
type, can be homegrown or you can get that from Arrow, can be anything. The reason to use Option instead of nulls is because null does not permit having a double nullables i.e. A??
isn’t possible, whereas in option it’s possible to have Option<A?>
this way you can have an arb that contain nullables, to construct desctructured map of Map<K, A?>
from it.
data class Value3<A, B, C>(val first: Option<A>, val second: Option<B>, third: Option<C>)
data class Map3<K, A, B, C>(val first: Map<K, A>, val second: Map<K, B>, val third: Map<K, C>)
fun <A, B, C> Arb.value3(
arbFirst: Arb<A>,
arbSecond: Arb<B>,
arbThird: Arb<C>,
... // other params that you'd want to add to determine the mixing ratios of A, B, and C, right now this assumes equal probability of mixing
): Arb<Value3<A, B, C>> = Arb.bind(
Arb.option(arbFirst),
Arb.option(arbSecond),
Arb.option(arbThird),
::Value3
)
fun <K, A, B, C> Map<K, Value3<A, B, C>>.destructured(): Map3<K, A, B, C> {
val firstMap = mutableMapOf<K, A>()
val secondMap = mutableMapOf<K, B>()
val thirdMap = mutableMapOf<K, C>()
this.forEach { key, (maybeFirst, maybeSecond, maybeThird) ->
maybeFirst.onSome { firstMap[key] = it }
maybeSecond.onSome { secondMap[key] = it }
maybeThird.onSome { thirdMap[key] = it }
}
Map3(firstMap, secondMap, thirdMap)
}
the arb of interest can therefore be written as follows, and then used in your test
val arbOfMapWithSharedKey: Arb<Map3<Key, Foo, Bar, Baz>> =
Arb
.map(arbKey, Arb.value3(arbFoo, arbBar, arbBaz))
.map { it.destructured() }
test("should handle maps with shared keys") {
checkAll(arbOfMapWithSharedKey) { (mapFoo, mapBar, mapBaz) -> // these maps will have shared keys
...
}
}
think of an Arb
as individual population of types. Each arb will be sampled independently to other arbs within the checkAll params. This means, if we were to have two arbs interrelated to one another, such has to be declared as part of the same population. This exercise is an example of what happens very often in more diverse and complex usecases.. In real use case, one would have various data classes and would have used either an Arb.bind
or flatMap
or the arbitrary { }
builder to create complex data classes and structures (e.g. maps and lists).
So yeah, sorry for the long and winded reply. My personal view would be not to add this to kotest, but rather just have this in the implementers’ personal repository. Hopefully that helps..mitch
03/19/2023, 12:21 PMabendt
03/19/2023, 4:48 PM