arrayskotlinassertionassertk

Is there a more concise way to assert that an array's elements each match a predicate?


We all know that you shouldn't assert exact equality for floating-point numbers. assertk comes with a convenient way to do it better:

assertThat(value).isCloseTo(3.0, 0.000001)

I'd like to extend that to arrays:

assertThat(array).isCloseTo(doubleArrayOf(2.0, 3.0), 0.000001)

But the best I can do is:

fun Assert<DoubleArray>.isCloseTo(expected: DoubleArray, delta: Double) {
    hasSameSizeAs(expected)
    this.matchesPredicate { actual : DoubleArray ->
        expected.indices.forEach { index ->
            if (!actual[index].isCloseTo(expected[index], epsilon)) {
                return@matchesPredicate false
            }
        }
        true
    }
}

fun Double.isCloseTo(expected: Double, epsilon: Double): Boolean {
    abs(this - expected) <= epsilon
}

Is there any way to reduce this matchesPredicate to something like (pseudocode):

    hasElementsMatchingPredicate { index, actual ->
        actual.isCloseTo(expected, epsilon)
    }

Solution

  • To answer the question in the title:

    assert that an array's elements each match a predicate

    You could write a new AssertK assertion for DoubleArray:

    fun Assert<DoubleArray>.allMatchPredicate(f: (Double) -> Boolean) = given { actual ->
        if (actual.all(f)) return
        val rejected = actual.filterNot(f)
        expected("all items to satisfy the predicate, but these items didn't: ${show(rejected)}")
    }
    

    Then use it like this:

    @Test
    fun test() {
        val positive = doubleArrayOf(1.0, 2.0, 3.0)
        val mixed = doubleArrayOf(-1.0, 1.0, -4.0)
    
        assertThat(positive).allMatchPredicate { it > 0.0 }
        assertThat(mixed).allMatchPredicate { it > 0.0 }
    }
    

    Result:

    org.opentest4j.AssertionFailedError: expected all items to satisfy the predicate, but these items didn't: <[-1.0, -4.0]>
    

    For comparing two DoubleArrays, as the question body asks for, you could do this:

    fun Assert<DoubleArray>.allCloseTo(expected: DoubleArray, delta: Double) = given { actual ->
        if (actual.size != expected.size)
            expected("array to have size ${expected.size} but was ${actual.size}")
    
        val rejected = actual.asSequence()
            .withIndex().filterNot {
                // adapted from Assert<Double>.isCloseTo
                it.value >= expected[it.index].minus(delta) &&
                    it.value <= expected[it.index].plus(delta)
            }
            .map { it.value }.toList()
    
        if (rejected.isNotEmpty())
            expected("all items to satisfy the predicate, but these items didn't: ${show(rejected)}")
    }
    

    Usage:

    @Test
    fun test() {
        val same = doubleArrayOf(1.001, 2.001, 3.001)
        val diff = doubleArrayOf(1.1, 2.001, 3.1)
    
        val expected = doubleArrayOf(1.0, 2.0, 3.0)
    
        assertThat(same).allCloseTo(expected, 0.01)
        assertThat(diff).allCloseTo(expected, 0.01)
    }
    

    Result:

    org.opentest4j.AssertionFailedError: expected all items to satisfy the predicate, but these items didn't: <[1.1, 3.1]>