Kotest Statistics
Statistics
Sometimes you may want to know what kinds of values Kotest generates to check that a generator is configured the way you want. Property test statistics are designed for this need.
The collect function is the starting point for statistics and is used to count categories of values. Use it by calling the function inside a property test with the category you want to increment.
For example, suppose you want to collect statistics for the RoundingMode values used by BigDecimal. Call checkAll as usual and pass the rounding mode to the collect function.
checkAll(Arb.enum<RoundingMode>(), Arb.bigDecimal()) { mode, decimal ->
collect(mode)
// test here
}
When the test completes, Kotest prints the test name and the count/percentage for each category:
Statistics: [collecting stats] (1000 iterations, 1 args)
HALF_DOWN 142 (14%)
HALF_UP 141 (14%)
CEILING 132 (13%)
FLOOR 122 (12%)
UP 119 (12%)
UNNECESSARY 119 (12%)
HALF_EVEN 118 (12%)
DOWN 107 (11%)
The categories you use do not have to be enums. They can be any object, and if you want more control, you can wrap them conditionally.
checkAll(Arb.int()) { k ->
when {
k % 2 == 0 -> collect("EVEN")
else -> collect("ODD")
}
// test here
}
Labels
Sometimes you may need orthogonal sets of statistics. For example, in a simple numeric test, you may want to check both that a certain percentage is even and that a certain percentage is negative. One way is to use EVEN_POS, EVEN_NEG, ODD_POS, and ODD_NEG.
checkAll(Arb.int()) { k ->
when {
k > 0 && k % 2 == 0 -> collect("EVEN_POS")
k % 2 == 0 -> collect("EVEN_NEG")
k > 0 -> collect("ODD_POS")
else -> collect("ODD_NEG")
}
// test here
}
This gives one output set:
EVEN_POS 142 (27%)
EVEN_NEG 141 (23%)
ODD_POS 132 (24%)
ODD_NEG 122 (26%)
However, as combinations increase, this becomes hard to manage, so Kotest supports labelled statistics. You can think of these as separate statistics sets. To use labels, simply pass the label name as the first argument to the collect method.
checkAll(Arb.int()) { k ->
when {
k % 2 == 0 -> collect("even_odd", "EVEN")
else -> collect("even_odd", "ODD")
}
when {
k > 0 -> collect("pos_neg", "POS")
else -> collect("pos_neg", "NEG")
}
// test here
}
Kotest now prints several statistics sets with the label name in the title:
Statistics: [collecting labelled stats] (1000 iterations, 1 args) [even_odd]
ODD 520 (52%)
EVEN 480 (48%)
Statistics: [collecting labelled stats] (1000 iterations, 1 args) [pos_neg]
NEG 527 (53%)
POS 473 (47%)
Report Mode
By default, statistics for all property tests are printed. There are four modes that can be configured with the global configuration object PropertyTesting.
The possible options are:
| Mode | Function |
|---|---|
PropertyTesting.statisticsReportMode = StatisticsReportMode.OFF |
Disables all statistics reporting. |
PropertyTesting.statisticsReportMode = StatisticsReportMode.ALL |
Enables all statistics reporting. |
PropertyTesting.statisticsReportMode = StatisticsReportMode.SUCCESS |
Outputs statistics only on successful tests. |
PropertyTesting.statisticsReportMode = StatisticsReportMode.FAILED |
Outputs statistics only on failed tests. |
Checking Statistics Coverage
To programmatically assert that certain values are being generated, you can specify constraints that must be satisfied.
For example, in the previous rounding example, you can use withCoveragePercentages to verify that at least 10% of inputs cover HALF_DOWN and 10% cover FLOOR.
withCoveragePercentages(mapOf(RoundingMode.HALF_DOWN to 10.0, RoundingMode.FLOOR to 10.0)) {
checkAll(Arb.enum<RoundingMode>(), Arb.bigDecimal()) { mode, decimal ->
collect(mode)
// use the mode / decimal
}
}
To check absolute counts instead of percentages, use withCoverageCounts.
withCoverageCounts(mapOf(RoundingMode.HALF_DOWN to 75, RoundingMode.FLOOR to 75)) {
checkAll(Arb.enum<RoundingMode>(), Arb.bigDecimal()) { mode, decimal ->
collect(mode)
// use the mode / decimal
}
}
Custom Reports
You can customize the report format or generate reports from raw data by using your own instance of a statistics reporter. This is configured through the global configuration object PropertyTesting.
For example:
object MyStatisticsReporter : object : StatisticsReporter { ... }
PropertyTesting.statisticsReporter = MyStatisticsReporter