assertj: Kotlin SAM overload resolution ambiguity between Consumer and ThrowingConsumer

Summary

When using AssertJ Core 3.21.0 from Kotlin, an overload resolution ambiguity occurrs for methods having overloads which accept both Consumer and ThrowingConsumer.

Methods affected (list noncomprehensive)

org.assertj.core.api.AbstractAssert#satisfies(java.util.function.Consumer<? super ACTUAL>)
org.assertj.core.api.ObjectEnumerableAssert#allSatisfy(org.assertj.core.api.ThrowingConsumer<? super ELEMENT>)

Example

Worked before (typical syntax for Kotlin calling a method which takes a functional interface AKA “SAM”)

assertThat(bounds).satisfies { // worked pre-3.21.0
    assertThat(it.startInclusive) // ... any detail assert statement
}

Wordy workaround:

assertThat(bounds).satisfies(Consumer {
    assertThat(it.startInclusive) // ... any detail assert statement
})

This overload ambiguity is (also) an issue of kotlin language design, cf. KT-17765

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 23
  • Comments: 30 (13 by maintainers)

Commits related to this issue

Most upvoted comments

Alright, let’s reopen this one.

I think it’s too late to introduce further breaking changes in version 3 for this topic, but version 4 may be the right time to rethink how to better support Kotlin.

I’ve created the following workaround in my tests. For the record, I still believe a more sensible approach would have been to name the new API satisfiesThrowable or similar.

/**
 * Convenience _AssertJ_ method used to verify that the receiver's actual value satisfies the specified test [requirements]
 * as a lambda block.
 *
 * *NOTE*: As of version `3.21.0` [AssertJ](https://assertj.github.io/doc/) [satisfies](https://tinyurl.com/yfkf9dcm)
 *   is irretrievably broken in Kotlin due to the rather unfortunate introduction of a method override using a
 *   [ThrowingConsumer](https://tinyurl.com/yek3fj4v) parameter which makes both methods indistinguishable while using
 *   single-abstraction-method (SAM) semantics.
 *
 * @param requirements Lambda which accepts a non-nullable instance of the _actual_ asserted object as an input parameter
 */
fun <T : Any> AbstractAssert<*, T>.satisfiesRequirements(requirements: (T) -> Unit): AbstractAssert<*, *> {
    return this.satisfies(requirements.toConsumer())
}

fun <E : Any, I : Iterable<E>> AbstractIterableAssert<*, I, E, *>.allSatisfyRequirements(requirements: (E) -> Unit): AbstractIterableAssert<*, *, *, *> {
    return this.allSatisfy(requirements.toConsumer())
}

private fun <T> ((T) -> Unit).toConsumer(): Consumer<T> {
    return Consumer { this(it) }
}

Kotlin 1.7 works with AssertJ 3.21.0. It won’t however work with AssertJ 3.22.0, as a new problem appears:

None of the following functions can be called with the arguments supplied: 
public final fun satisfies(vararg p0: Consumer<in String!>!): ObjectAssert<String!>! defined in org.assertj.core.api.ObjectAssert
public final fun satisfies(vararg p0: ThrowingConsumer<in String!>!): ObjectAssert<String!>! defined in org.assertj.core.api.ObjectAssert
public open fun satisfies(p0: Condition<in String!>!): ObjectAssert<String!>! defined in org.assertj.core.api.ObjectAssert

Note this is the same satisfies method but a different error message than the one originally reported here. Also the reason is different: Kotlin compiler is unable to properly handle vararg use of Consumer and ThrowingConsumer. Reported as a separate issue: KT-52942. If you found my comment because you experience this new problem, upvote the issue in Jetbrains’ issue tracker.

I had the same problem after I updated spring boot. For me, the problem was solved by downgrade assertj-core to version 3.20.2 (testImplementation("org.assertj:assertj-core:3.20.2")). I guess that’s not a long-term solution, but a workaround for now.

Though KT-17765 seems fixed in Kotlin 1.6.20-M1 it requires using experimental language version 1.7, e.g.:

tasks.compileTestKotlin {
    kotlinOptions.languageVersion = "1.7"
}

My two cents:

import org.assertj.core.api.AbstractAssert
import org.assertj.core.api.AbstractIterableAssert
import java.util.function.Consumer

@Suppress("UNCHECKED_CAST")
fun <E : Any?, I : Iterable<E>> AbstractIterableAssert<*, I, E, *>.allSatisfyKt(requirements: Consumer<E>): AbstractIterableAssert<*, I, E, *> {
    return this.allSatisfy(requirements) as AbstractIterableAssert<*, I, E, *>
}

@Suppress("UNCHECKED_CAST")
fun <E : Any?, I : Iterable<E>> AbstractIterableAssert<*, I, E, *>.anySatisfyKt(requirements: Consumer<E>): AbstractIterableAssert<*, I, E, *> {
    return this.anySatisfy(requirements) as AbstractIterableAssert<*, I, E, *>
}

@Suppress("UNCHECKED_CAST")
fun <T : Any?> AbstractAssert<*, T>.satisfiesKt(requirements: Consumer<T>): AbstractAssert<*, T> {
    return this.satisfies(requirements) as AbstractAssert<*, T>
}

Though KT-17765 seems fixed in Kotlin 1.6.20-M1 it requires using experimental language version 1.7, e.g.:

tasks.compileTestKotlin {
    kotlinOptions.languageVersion = "1.7"
}

I’ve tested using experimental language version 1.7 as suggested and it works… kinda idea

As can be seen in the attached image, braces around the Consumer lambda are still required…

KT-17765 is now fixed and is targeted for 1.6.20 🎉

See also https://youtrack.jetbrains.com/issue/KT-53113/Kotlin-AssertJ-tests-compile-in-1621-but-not-in-17

It’s an disturbing blame game between AssertJ and Kotlin with fingerpointing on both sides but still no solution, even with Kotlin 1.7. Besides a manual downgrade to 3.20.2 (3.21 did not work):

    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(module = "assertj-core") // Downgrade as temp. workaround for https://github.com/assertj/assertj-core/issues/2357
    }
    testImplementation("org.assertj:assertj-core:3.20.2") // See above

AssertJ (as Bundled with Spring Boot 2.6+) does simply not work with Kotlin currently.

As can be seen in the attached image, braces around the Consumer lambda are still required…

Is that just in the IDE or do you also get compilation errors?

If your IDE Kotlin plugin version is out of date that might result in what you’re seeing, I assume you would need IDE plugin version 1.6.20 or greater.

Compilation error occurs during Gradle build as well… I am using Kotlin Plugin 213-1.6.20-release-275-IJ6777.52

Screen Shot 2022-04-11 at 11 38 53 am

I do not think that this can be fixed in Kotlin. It is not a bug in the language. To identify SAM interfaces, we can take arguments to the called function and the result type into consideration. If after that, more than one interface matches, we are out of luck. Now exceptions are simply not taken into consideration here – and this is by design.

I somehow seem to remember a similar issue with .extracting() – was there someting like a ThrowingFunction once upon a time?

BTW, the work-around is super-ugly with our standard formatter, which will put the Consumer { on a line by itself and requires another level of indentation. I wrote a satifisfiesSoftly() extension function just to avoid this (and add softness on top). I would very much wish for AssertJ to stay compatible with Kotlin. I know there are other assertion libraries, but they are just not up to par.