Fadelis / grpcmock

A gRPC Java testing tool to easily mock endpoints of gRPC services for IT or Unit testing
http://grpcmock.org
Apache License 2.0
142 stars 13 forks source link

Doesn't seem to work in reactive applications? #33

Closed edeandrea closed 8 months ago

edeandrea commented 8 months ago

I'm trying to incorporate this library into an application that is using Quarkus and gRPC. I think there is an overall design flaw which prevents this library from being able to be used...

In the GrpcMock class it uses a ThreadLocal for the instance to use:

private static final ThreadLocal<GrpcMock> INSTANCE = ThreadLocal
      .withInitial(() -> grpcMock().build());

When running in Quarkus or on top of Vertx, the thread that the stub was created on and the thread which actually executes the request are 2 different threads, therefore the stubs are never able to be looked up because of the fact that all the storage is based on a ThreadLocal.

edeandrea commented 8 months ago

I see that WireMock itself does this, but it also offers instance methods that don't all delegate to the ThreadLocal. Thats why WireMock works in my case. I create an instance and then re-use that instance across all threads rather than use static methods that look things up in the ThreadLocal.

It seems the stubFor and verifyThat methods in GrpcMock are static and look up the instance in the ThreadLocal. I think thats a key difference and why WireMock works for me but this does not.

Unfortunately I can't use this as is, which is too bad, because I feel like it's much more mature and has more features than the Wiremock gRPC extension.

Fadelis commented 8 months ago

Hi @edeandrea, you are correct that when working in setups where the static instance is configured in one thread and stubs in another one it won't work. In these cases you need to auto wire or create an instance of grpcmock in the test class and then use the methods directly in this instance to register stubs instead of the static method stubFor. Same for verifyThat.

edeandrea commented 8 months ago

Thanks for the response @Fadelis. The GrpcMock class itself doesn't have any instance methods for stubFor. They're all static and use the ThreadLocal, which is different than WireMock. WireMock does provide instance methods as well as static methods.

Fadelis commented 8 months ago

Please look carefully into the public methods that are exposed in the GrpcMock class. There are equivalents for both stubFor and verifyThat, as the static methods call them.

edeandrea commented 8 months ago

I only see 1 stubFor method on the GrpcMock class, and that method is static...

image

edeandrea commented 8 months ago

Aha! I'm sorry I was assuming it had the same name! I didn't realize it was called something different!

I'll give that a try. Thank you!

Fadelis commented 8 months ago

But do you see the method that is called on the GrpcMock instance register?

edeandrea commented 8 months ago

Thank you @Fadelis that did work, although the verification is failing...

15:03:03 DEBUG [io.qu.sa.su.fi.cl.LocationClient] (main) Making request to location service for hello operation
15:03:03 INFO  [io.qu.gr.ru.su.Channels] (main) Creating Vert.x gRPC channel ...
15:03:03 INFO  [or.gr.GrpcMock] (grpc-default-executor-0) 
----------------------------------------
Received request:
Request to io.quarkus.sample.superheroes.location.v1.Locations/Hello method
with headers:
Metadata(traceparent=00-930240576ddf87fdadf2f0c5bf918c93-720e43c6713c70fd-01,content-type=application/grpc,grpc-accept-encoding=gzip,traceparent=00-61e707d5b6ac3fae72b779c533e3b091-9b56def3391cc17a-01)
with request:

----------------------------------------
15:03:03 DEBUG [io.qu.sa.su.fi.cl.LocationClient] (vert.x-eventloop-thread-1) Got response back from location service: Hello locations!

org.grpcmock.exception.GrpcMockVerificationError: Expected io.quarkus.sample.superheroes.location.v1.Locations/Hello method to be called exactly 1 times, but actual call count was 0

    at org.grpcmock.GrpcMock.verifyThat(GrpcMock.java:164)
    at org.grpcmock.GrpcMock.verifyThat(GrpcMock.java:431)
    at org.grpcmock.GrpcMock.verifyThat(GrpcMock.java:404)
    at io.quarkus.sample.superheroes.fight.client.LocationClientTests.helloLocations(LocationClientTests.java:95)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at io.quarkus.test.junit.QuarkusTestExtension.runExtensionMethod(QuarkusTestExtension.java:1014)
    at io.quarkus.test.junit.QuarkusTestExtension.interceptTestMethod(QuarkusTestExtension.java:828)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

This is the test:

@Test
public void helloLocations() {
    this.grpcMock.register(
        unaryMethod(LocationsGrpc.getHelloMethod())
            .willReturn(HelloReply.newBuilder().setMessage(DEFAULT_HELLO_RESPONSE).build())
    );

    this.locationClient.helloLocations()
        .subscribe().withSubscriber(UniAssertSubscriber.create())
                .assertSubscribed()
               .awaitItem(Duration.ofSeconds(5))
              .assertItem(DEFAULT_HELLO_RESPONSE);

    this.grpcMock.verifyThat(
        calledMethod(LocationsGrpc.getHelloMethod())
            .withRequest(HelloRequest.newBuilder().build())
    );
}
edeandrea commented 8 months ago

It looks like that the verifyThat instance method delegates to the static method, which looks things up in the ThreadLocal...

edeandrea commented 8 months ago

It looks like to get around that I have to use the only non-static method, which forces me to build the request matcher myself...

This works:

@Test
public void helloLocations() {
    this.grpcMock.register(
        unaryMethod(LocationsGrpc.getHelloMethod())
            .withRequest(HelloRequest.newBuilder().build())
            .willReturn(HelloReply.newBuilder().setMessage(DEFAULT_HELLO_RESPONSE).build())
    );

    this.locationClient.helloLocations()
        .subscribe().withSubscriber(UniAssertSubscriber.create())
                .assertSubscribed()
                .awaitItem(Duration.ofSeconds(5))
                .assertItem(DEFAULT_HELLO_RESPONSE);

    this.grpcMock.verifyThat(
        calledMethod(LocationsGrpc.getHelloMethod())
            .withRequest(HelloRequest.newBuilder().build())
            .build(),
        times(1)
    );
}
Fadelis commented 8 months ago

Yes, as mentioned before you can only use non static methods in such setups. Decent java IDE should inform/highlight when doing silly mistakes like calling static methods on an instance object. Glad you were able to figure it out 👍

Fadelis commented 8 months ago

Btw, one one other way to solve this issue and be able to use the static methods, would be to call GrpcMock.configureFor method and passing your grpcMock instance at the beginning of your test.

@Test
public void helloLocations() {
  configureFor(this.grpcMock);
  ... 
  // use static methods
}

Depending whether BeforeEach method is called on the same thread as the test method, you could also do that in the BeforeEach method.


@BeforeEach
void setup() {
  configureFor(this.grpcMock);
}
edeandrea commented 8 months ago

I tried that. The issue seems to be that the test and stub creation is happening on one thread, yet the incoming request is handled on another thread. The test itself doesn't use the grpc client directly. It's testing an operation on the application under test that happens to use a grpc client, which is on a different thread from where the stub was created.

Fadelis commented 8 months ago

That's strange, once the server is started it should listen on a specific port. Then creating a ManagedChannel using that port it shouldn't matter from which thread you make the gRPC request as it should go through the network

edeandrea commented 8 months ago

I don't know/understand all the inner details of how gRPC in Quarkus/Vert.x works, but when I was debugging/breakpoints/etc, the thread that created the stub and the thread that was handling the request/looking for the stub was different.

I've gotten it to work though using the instance methods rather than the static methods though.

Fadelis commented 8 months ago

Maybe if you'll have time you could create a small app in github to replicate this. Then it would be possible to investigate the problem.

edeandrea commented 8 months ago

https://github.com/edeandrea/quarkus-super-heroes/blob/add-location-service/rest-fights/src/test/java/io/quarkus/sample/superheroes/fight/client/LocationClientTests.java is the test class. Right now its using the instance methods so that it works.

https://github.com/edeandrea/quarkus-super-heroes/blob/add-location-service/rest-fights/src/test/java/io/quarkus/sample/superheroes/fight/GrpcMockServerResource.java is where the GrpcMock class is initialized.

Fadelis commented 8 months ago

Yeah, this is not what I'd call a small project, and I don't want to invest too much time to set it up (there are a lot of errors due to testcontainers and kube just to run the tests), but for me it seems you're having the GrpcMock.configureFor(this.server); at the wrong place, you should call it directly in the test method not in the Resource class.

edeandrea commented 8 months ago

You just need a container runtime (docker/podman) running and everything should work fine.

I did try moving that line into the test method itself but got the same result. The test and the test resource class are both run on the same thread - the main thread, yet when the mock server is handling requests it's on a different thread.

And then the gRPC client is on yet another thread.

edeandrea commented 8 months ago

You just need a container runtime (docker/podman) running and everything should work fine.

I did try moving that line into the test method itself but got the same result. The test and the test resource class are both run on the same thread - the main thread, yet when the mock server is handling requests it's on a different thread.

The gRPC client is on yet another thread.

For what it's worth, when I try using WireMock using it's static methods I see the same errors.