Hi folks, I was working on some property tests fo...
# kotest
a
Hi folks, I was working on some property tests for code that needs two maps
Map<A,B>
for input. Initially i specified the maps like this:
Copy code
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:
Copy code
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.
Copy code
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?
e
The use-case makes sense.. I'm not sure if there's tools to tackle it out of the box currently. If we need to make additions to support it, I'm thinking it would be nice if one could compose the following:
Copy code
fun <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?
Maybe a good idea to move this to issues, so it doesn't get lost in the channel
Interesting idea to add more functions to help build such an Arb 👍🏾 i have created an issue, also have linked an implementation with my approach.
m
Thanks @abendt for raising the issue and a PR of your approach! really appreciate it. Thinking out loud wouldn’t this then mean that at some point we’d have
Arb.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?
I'm just looking at your implementation and from the look of it it seems like a special case of Arb.map, where the value being a sealed type of A, B or both A & B. Something like
Arb<Map<K, Value2<A, B>>
. Here you can imagine that Value2 is a sealed class being
First<A>
Second<B>
or
Both<A, B>
Arrow has a type called Ior that is exactly that
Now imagine in your test you can create a simple function that split that into individual maps:
fun <K, A, B > Map<K, Value2<A, B>>.split(): Pair<Map<K, A>, Map<K, B>> = ...
That means you can simply use Arb.map and Arb.value2 as follows:
Arb.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.
@abendt Having said that, my humble opinion would be not to add that into kotest... This arb is very specific to very niche usecase. As it stands, there are many ways to solve this.. either a concrete sealed class or a data class with options (since nulls doesn’t stack… 😞 ).. Many ways. This is something I just wrote on slack within the last 10 minutes. Here I’m assuming you’d have access to an
Option<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.
Copy code
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
Copy code
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..
I’ll also comment in the issue accordingly
a
thanks @mitch for that elaborate and instructive response!