Mikhail Konovalov opened SPR-15692 and commented

It seems that due to recursive generics in BodySpec interface

interface BodySpec<B, S extends BodySpec<B, S>>

and due to expectBody method returns

<B> BodySpec<B, ?> expectBody(Class<B> bodyType);

WebTestClient cannot be used in Kotlin.

Kotlin inherits the result of .expectBody(Person::class.java) as BodySpec<Person, *> and thus the following methods in chain cannot be constructed due to the following error:

Error:(25, 20) Kotlin: Type inference failed: Not enough information to infer parameter T in fun isEqualTo(p0: Controller.Person!): T! Please specify it explicitly.

And it applies only Nothing as a type parameter. But in this case generated bytecode contains the following line

throw null

Example:

@Test
fun `test get`() {
    val expectBody: BodySpec<Person, *> = client.get().uri("/person/42").exchange()
            .expectBody(Person::class.java)
    expectBody.isEqualTo(Person("42", "Ivan"))                            // doesn't compile here
    expectBody.isEqualTo<BodySpec<Person, *>>(Person("42", "Ivan"))       // doesn't compile here
    expectBody.isEqualTo<Nothing>(Person("42", "Ivan"))                   // compile but lead to "throw null" in bytecode
}

If you work with list the situation is a bit better - Kotlin still cannot inherit type param automatically but you can specify it explicitly due to method expectBodyList in interface ListBodySpec doesn't return wildcards

<E> ListBodySpec<E> expectBodyList(Class<E> elementType);

Example:

@Test
    fun `test list`() {
        val expectBodyList: ListBodySpec<Person> = client.get().uri("/person").exchange()
                .expectBodyList(Person::class.java)
        expectBodyList.consumeWith<ListBodySpec<Person>> { list -> Assert.assertTrue(true) }   // need to specify type param explicitly
    }

Full example with java and kotlin can be found here. Tests in java works well in these cases.


Affects: 5.0 RC2

Reference URL: https://gist.github.com/mskonovalov/42761bbc548e92c2af16c40cffcfcaf3

Issue Links: - #20606 Unable to use WebTestClient with mock server in Kotlin

Referenced from: commits https://github.com/spring-projects/spring-framework/commit/91c8b62817facb1e7b25c070f0f29b8ae0e23c3d, https://github.com/spring-projects/spring-framework/commit/568a0b5b79ab170d5f9ea96df43ea9560f93f9ec

0 votes, 5 watchers

Comment From: spring-projects-issues

Sébastien Deleuze commented

Good catch, I have raised the point on Kotlin issue tracker (see this KT-5464 comment) cc Rossen Stoyanchev.

Comment From: spring-projects-issues

Mikhail Konovalov commented

I'm not sure the problem is with Kotlin compiler. Maybe we can avoid somehow wildcard recursive type params?

Comment From: spring-projects-issues

Mikhail Konovalov commented

Sébastien Deleuze, I've used your approach you've mentioned in KT-5464 and it started to compile

@Test
    fun `test get 3`() {
        val bar: BodySpec<Person, *> = client.get().uri("/person/27").exchange()
                .expectBody(Person::class.java).consumeWith { person -> Assert.assertTrue(true) }  // compile but leads to NPE
    }

But if I generate Kotlin bytecode and then decompile it I get

@Test
   public final void test_get_3/* $FF was: test get 3*/() {
      this.client.get().uri("/person/27", new Object[0]).exchange().expectBody(Person.class).consumeWith((Consumer)null.INSTANCE);
      throw null;
   }

(also updated the gist with new case )

Comment From: spring-projects-issues

Sébastien Deleuze commented

Latest comment seems to show this is an issue on Kotlin side that could be fixed in an upcoming Kotlin major version. They also suggest a workaround expectBody.isEqualTo<Nothing?>(Person("42", "Ivan")). They are going to provide a roadmap shortly.

Comment From: spring-projects-issues

Mikhail Konovalov commented

Looks like fixes NPE but call chain will be finished on this. Don't you think it can be fixed if redesign this

interface BodySpec<B, S extends BodySpec<B, S>>

Comment From: spring-projects-issues

Sébastien Deleuze commented

Not sure, do you have a alternative proposal?

It seems they plan to fix this in Kotlin 1.2 or 1.3, so without any concrete proposal / PR to discuss with Rossen Stoyanchev, I tend to think we should not enforce artificially another design for a temporary Kotlin issue.

Comment From: spring-projects-issues

Mikhail Konovalov commented

I agree not to break everything for Kotlin issue. But for me it looks not perfect in Java way too. I mean this

BodySpec<B, ?>

It looks like we create strict recursive structure in interface and then relax it with wildcards. I'll try to think of the proposal, but I think it'd be too late to change considering GA.

Comment From: spring-projects-issues

Sébastien Deleuze commented

KT-5464 is expected to be fixed for Kotlin 1.3, and is likely to fix the issue described here, so I prefer keep things as they are if Kotlin is the only relevant reason. Please vote for KT-5464 in order to make sure it will remain high priority.

Comment From: spring-projects-issues

Sébastien Deleuze commented

I am reopening this issue since it seems we can provide a workaround for this very annoying issue by providing a Kotlin extension calling .expectBody<String>().returnResult().apply as demonstrated on https://github.com/sdeleuze/webflux-kotlin-web-tests/.

Comment From: spring-projects-issues

Sébastien Deleuze commented

Resolved by updating the expectBody Kotlin extension to use a Kotlin compliant API with implementation similar to the Java one based on expectBody(foo::class.java).returnResult().

Comment From: spring-projects-issues

Sébastien Deleuze commented

Side note: be aware of the lack of autocomplete on expectBody Kotlin extension, it has been raised as KT-23834 to JetBrains.

Comment From: cristianprofile

Is there any example how to use interface BodySpec> using WebTestClient with Kotlin similar to expectBodyList?

Comment From: sdeleuze

Please use stackoverflow for such question, thanks.

Comment From: noah-iam

Hey, For the similar issue , I want to share you my piece of code that is giving me same error :

.webFilter<>(myfilter). This is saying to give the generic type here.

Error : Type expected

val client: WebTestClient = WebTestClient.bindToWebHandler { Mono.empty() } .webFilter<>(myfilter) .build()

Error : Type argument is not within its bounds. Expected: Nothing! Found: WebFilter!

@sdeleuze can you help me in this .