diff --git a/examples/README.md b/examples/README.md index fcf45e7cfa..cc39de459d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -44,3 +44,23 @@ $ mvn exec:java -Dexec.mainClass=io.grpc.examples.helloworld.HelloWorldServer $ # In another terminal run the client $ mvn exec:java -Dexec.mainClass=io.grpc.examples.helloworld.HelloWorldClient ``` + +Unit test examples +============================================== + +Examples for unit testing gRPC clients and servers are located in [examples/src/test](src/test). + +In general, we DO NOT allow overriding the client stub. +We encourage users to leverage `InProcessTransport` as demonstrated in the examples to +write unit tests. `InProcessTransport` is light-weight and runs the server +and client in the same process without any socket/TCP connection. + +For testing a gRPC client, create the client with a real stub +using an +[InProcessChannel](../core/src/main/java/io/grpc/inprocess/InProcessChannelBuilder.java), +and test it against an +[InProcessServer](../core/src/main/java/io/grpc/inprocess/InProcessServerBuilder.java) +with a mock/fake service implementation. + +For testing a gRPC server, create the server as an InProcessServer, +and test it against a real client stub with an InProcessChannel. diff --git a/examples/build.gradle b/examples/build.gradle index 2daf7e3590..74a44b7c5e 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -28,6 +28,9 @@ dependencies { compile "io.grpc:grpc-netty:${grpcVersion}" compile "io.grpc:grpc-protobuf:${grpcVersion}" compile "io.grpc:grpc-stub:${grpcVersion}" + + testCompile "junit:junit:4.11" + testCompile "org.mockito:mockito-core:1.9.5" } protobuf { diff --git a/examples/pom.xml b/examples/pom.xml index 35d61bc3ec..ca695af280 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -28,6 +28,18 @@ grpc-stub ${grpc.version} + + junit + junit + 4.11 + test + + + org.mockito + mockito-core + 1.9.5 + test + diff --git a/examples/src/main/java/io/grpc/examples/advanced/HelloJsonServer.java b/examples/src/main/java/io/grpc/examples/advanced/HelloJsonServer.java index a6a742cccc..b328cf54a8 100644 --- a/examples/src/main/java/io/grpc/examples/advanced/HelloJsonServer.java +++ b/examples/src/main/java/io/grpc/examples/advanced/HelloJsonServer.java @@ -61,11 +61,11 @@ import java.util.logging.Logger; public class HelloJsonServer { private static final Logger logger = Logger.getLogger(HelloWorldServer.class.getName()); - /* The port on which the server should run */ - private int port = 50051; private Server server; private void start() throws IOException { + /* The port on which the server should run */ + int port = 50051; server = ServerBuilder.forPort(port) .addService(new GreeterImpl()) .build() diff --git a/examples/src/main/java/io/grpc/examples/errorhandling/DetailErrorSample.java b/examples/src/main/java/io/grpc/examples/errorhandling/DetailErrorSample.java index a137b865d8..25fdc25577 100644 --- a/examples/src/main/java/io/grpc/examples/errorhandling/DetailErrorSample.java +++ b/examples/src/main/java/io/grpc/examples/errorhandling/DetailErrorSample.java @@ -86,11 +86,10 @@ public class DetailErrorSample { new DetailErrorSample().run(); } - private Server server; private ManagedChannel channel; void run() throws Exception { - server = ServerBuilder.forPort(0).addService(new GreeterGrpc.GreeterImplBase() { + Server server = ServerBuilder.forPort(0).addService(new GreeterGrpc.GreeterImplBase() { @Override public void sayHello(HelloRequest request, StreamObserver responseObserver) { Metadata trailers = new Metadata(); diff --git a/examples/src/main/java/io/grpc/examples/errorhandling/ErrorHandlingClient.java b/examples/src/main/java/io/grpc/examples/errorhandling/ErrorHandlingClient.java index 64b697e2c7..bdfdb8ed12 100644 --- a/examples/src/main/java/io/grpc/examples/errorhandling/ErrorHandlingClient.java +++ b/examples/src/main/java/io/grpc/examples/errorhandling/ErrorHandlingClient.java @@ -67,11 +67,11 @@ public class ErrorHandlingClient { new ErrorHandlingClient().run(); } - private Server server; private ManagedChannel channel; void run() throws Exception { - server = ServerBuilder.forPort(0).addService(new GreeterGrpc.GreeterImplBase() { + // Port 0 means that the operating system will pick an available port to use. + Server server = ServerBuilder.forPort(0).addService(new GreeterGrpc.GreeterImplBase() { @Override public void sayHello(HelloRequest request, StreamObserver responseObserver) { responseObserver.onError(Status.INTERNAL diff --git a/examples/src/main/java/io/grpc/examples/header/HeaderClientInterceptor.java b/examples/src/main/java/io/grpc/examples/header/HeaderClientInterceptor.java index c9b1f12791..a7042e33c1 100644 --- a/examples/src/main/java/io/grpc/examples/header/HeaderClientInterceptor.java +++ b/examples/src/main/java/io/grpc/examples/header/HeaderClientInterceptor.java @@ -31,6 +31,8 @@ package io.grpc.examples.header; +import com.google.common.annotations.VisibleForTesting; + import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; @@ -49,7 +51,8 @@ public class HeaderClientInterceptor implements ClientInterceptor { private static final Logger logger = Logger.getLogger(HeaderClientInterceptor.class.getName()); - private static Metadata.Key customHeadKey = + @VisibleForTesting + static final Metadata.Key CUSTOM_HEADER_KEY = Metadata.Key.of("custom_client_header_key", Metadata.ASCII_STRING_MARSHALLER); @Override @@ -60,13 +63,13 @@ public class HeaderClientInterceptor implements ClientInterceptor { @Override public void start(Listener responseListener, Metadata headers) { /* put custom header */ - headers.put(customHeadKey, "customRequestValue"); + headers.put(CUSTOM_HEADER_KEY, "customRequestValue"); super.start(new SimpleForwardingClientCallListener(responseListener) { @Override public void onHeaders(Metadata headers) { /** * if you don't need receive header from server, - * you can use {@link io.grpc.stub.MetadataUtils attachHeaders} + * you can use {@link io.grpc.stub.MetadataUtils#attachHeaders} * directly to send header */ logger.info("header received from server:" + headers); diff --git a/examples/src/main/java/io/grpc/examples/header/HeaderServerInterceptor.java b/examples/src/main/java/io/grpc/examples/header/HeaderServerInterceptor.java index c9e9b4d80f..2d1f52a9cf 100644 --- a/examples/src/main/java/io/grpc/examples/header/HeaderServerInterceptor.java +++ b/examples/src/main/java/io/grpc/examples/header/HeaderServerInterceptor.java @@ -31,6 +31,8 @@ package io.grpc.examples.header; +import com.google.common.annotations.VisibleForTesting; + import io.grpc.ForwardingServerCall.SimpleForwardingServerCall; import io.grpc.Metadata; import io.grpc.ServerCall; @@ -46,7 +48,8 @@ public class HeaderServerInterceptor implements ServerInterceptor { private static final Logger logger = Logger.getLogger(HeaderServerInterceptor.class.getName()); - private static Metadata.Key customHeadKey = + @VisibleForTesting + static final Metadata.Key CUSTOM_HEADER_KEY = Metadata.Key.of("custom_server_header_key", Metadata.ASCII_STRING_MARSHALLER); @@ -59,7 +62,7 @@ public class HeaderServerInterceptor implements ServerInterceptor { return next.startCall(new SimpleForwardingServerCall(call) { @Override public void sendHeaders(Metadata responseHeaders) { - responseHeaders.put(customHeadKey, "customRespondValue"); + responseHeaders.put(CUSTOM_HEADER_KEY, "customRespondValue"); super.sendHeaders(responseHeaders); } }, requestHeaders); diff --git a/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldClient.java b/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldClient.java index 4b755da643..fc2e1672e0 100644 --- a/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldClient.java +++ b/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldClient.java @@ -50,11 +50,15 @@ public class HelloWorldClient { /** Construct client connecting to HelloWorld server at {@code host:port}. */ public HelloWorldClient(String host, int port) { - channel = ManagedChannelBuilder.forAddress(host, port) + this(ManagedChannelBuilder.forAddress(host, port) // Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid // needing certificates. - .usePlaintext(true) - .build(); + .usePlaintext(true)); + } + + /** Construct client for accessing RouteGuide server using the existing channel. */ + HelloWorldClient(ManagedChannelBuilder channelBuilder) { + channel = channelBuilder.build(); blockingStub = GreeterGrpc.newBlockingStub(channel); } diff --git a/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java b/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java index d88bbef3e9..ce1158a43d 100644 --- a/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java +++ b/examples/src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java @@ -44,11 +44,11 @@ import java.util.logging.Logger; public class HelloWorldServer { private static final Logger logger = Logger.getLogger(HelloWorldServer.class.getName()); - /* The port on which the server should run */ - private int port = 50051; private Server server; private void start() throws IOException { + /* The port on which the server should run */ + int port = 50051; server = ServerBuilder.forPort(port) .addService(new GreeterImpl()) .build() @@ -89,7 +89,7 @@ public class HelloWorldServer { server.blockUntilShutdown(); } - private class GreeterImpl extends GreeterGrpc.GreeterImplBase { + static class GreeterImpl extends GreeterGrpc.GreeterImplBase { @Override public void sayHello(HelloRequest req, StreamObserver responseObserver) { diff --git a/examples/src/main/java/io/grpc/examples/routeguide/RouteGuideClient.java b/examples/src/main/java/io/grpc/examples/routeguide/RouteGuideClient.java index 8828beb37c..64066995a1 100644 --- a/examples/src/main/java/io/grpc/examples/routeguide/RouteGuideClient.java +++ b/examples/src/main/java/io/grpc/examples/routeguide/RouteGuideClient.java @@ -31,6 +31,9 @@ package io.grpc.examples.routeguide; +import com.google.common.annotations.VisibleForTesting; +import com.google.protobuf.Message; + import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.Status; @@ -58,6 +61,9 @@ public class RouteGuideClient { private final RouteGuideBlockingStub blockingStub; private final RouteGuideStub asyncStub; + private Random random = new Random(); + private TestHelper testHelper; + /** Construct client for accessing RouteGuide server at {@code host:port}. */ public RouteGuideClient(String host, int port) { this(ManagedChannelBuilder.forAddress(host, port).usePlaintext(true)); @@ -85,8 +91,14 @@ public class RouteGuideClient { Feature feature; try { feature = blockingStub.getFeature(request); + if (testHelper != null) { + testHelper.onMessage(feature); + } } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus()); + warning("RPC failed: {0}", e.getStatus()); + if (testHelper != null) { + testHelper.onRpcError(e); + } return; } if (RouteGuideUtil.exists(feature)) { @@ -116,17 +128,19 @@ public class RouteGuideClient { Iterator features; try { features = blockingStub.listFeatures(request); + for (int i = 1; features.hasNext(); i++) { + Feature feature = features.next(); + info("Result #" + i + ": {0}", feature); + if (testHelper != null) { + testHelper.onMessage(feature); + } + } } catch (StatusRuntimeException e) { - logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus()); - return; + warning("RPC failed: {0}", e.getStatus()); + if (testHelper != null) { + testHelper.onRpcError(e); + } } - - StringBuilder responseLog = new StringBuilder("Result: "); - while (features.hasNext()) { - Feature feature = features.next(); - responseLog.append(feature); - } - info(responseLog.toString()); } /** @@ -143,12 +157,17 @@ public class RouteGuideClient { info("Finished trip with {0} points. Passed {1} features. " + "Travelled {2} meters. It took {3} seconds.", summary.getPointCount(), summary.getFeatureCount(), summary.getDistance(), summary.getElapsedTime()); + if (testHelper != null) { + testHelper.onMessage(summary); + } } @Override public void onError(Throwable t) { - Status status = Status.fromThrowable(t); - logger.log(Level.WARNING, "RecordRoute Failed: {0}", status); + warning("RecordRoute Failed: {0}", Status.fromThrowable(t)); + if (testHelper != null) { + testHelper.onRpcError(t); + } finishLatch.countDown(); } @@ -162,15 +181,14 @@ public class RouteGuideClient { StreamObserver requestObserver = asyncStub.recordRoute(responseObserver); try { // Send numPoints points randomly selected from the features list. - Random rand = new Random(); for (int i = 0; i < numPoints; ++i) { - int index = rand.nextInt(features.size()); + int index = random.nextInt(features.size()); Point point = features.get(index).getLocation(); info("Visiting point {0}, {1}", RouteGuideUtil.getLatitude(point), RouteGuideUtil.getLongitude(point)); requestObserver.onNext(point); // Sleep for a bit before sending the next one. - Thread.sleep(rand.nextInt(1000) + 500); + Thread.sleep(random.nextInt(1000) + 500); if (finishLatch.getCount() == 0) { // RPC completed or errored before we finished sending. // Sending further requests won't error, but they will just be thrown away. @@ -186,15 +204,17 @@ public class RouteGuideClient { requestObserver.onCompleted(); // Receiving happens asynchronously - finishLatch.await(1, TimeUnit.MINUTES); + if (!finishLatch.await(1, TimeUnit.MINUTES)) { + warning("recordRoute can not finish within 1 minutes"); + } } /** * Bi-directional example, which can only be asynchronous. Send some chat messages, and print any * chat messages that are sent from the server. */ - public void routeChat() throws InterruptedException { - info("*** RoutChat"); + public CountDownLatch routeChat() { + info("*** RouteChat"); final CountDownLatch finishLatch = new CountDownLatch(1); StreamObserver requestObserver = asyncStub.routeChat(new StreamObserver() { @@ -202,12 +222,17 @@ public class RouteGuideClient { public void onNext(RouteNote note) { info("Got message \"{0}\" at {1}, {2}", note.getMessage(), note.getLocation() .getLatitude(), note.getLocation().getLongitude()); + if (testHelper != null) { + testHelper.onMessage(note); + } } @Override public void onError(Throwable t) { - Status status = Status.fromThrowable(t); - logger.log(Level.WARNING, "RouteChat Failed: {0}", status); + warning("RouteChat Failed: {0}", Status.fromThrowable(t)); + if (testHelper != null) { + testHelper.onRpcError(t); + } finishLatch.countDown(); } @@ -236,8 +261,8 @@ public class RouteGuideClient { // Mark the end of requests requestObserver.onCompleted(); - // Receiving happens asynchronously - finishLatch.await(1, TimeUnit.MINUTES); + // return the latch while receiving happens asynchronously + return finishLatch; } /** Issues several different requests and then exits. */ @@ -265,18 +290,55 @@ public class RouteGuideClient { client.recordRoute(features, 10); // Send and receive some notes. - client.routeChat(); + CountDownLatch finishLatch = client.routeChat(); + + if (!finishLatch.await(1, TimeUnit.MINUTES)) { + client.warning("routeChat can not finish within 1 minutes"); + } } finally { client.shutdown(); } } - private static void info(String msg, Object... params) { + private void info(String msg, Object... params) { logger.log(Level.INFO, msg, params); } + private void warning(String msg, Object... params) { + logger.log(Level.WARNING, msg, params); + } + private RouteNote newNote(String message, int lat, int lon) { return RouteNote.newBuilder().setMessage(message) .setLocation(Point.newBuilder().setLatitude(lat).setLongitude(lon).build()).build(); } + + /** + * Only used for unit test, as we do not want to introduce randomness in unit test. + */ + @VisibleForTesting + void setRandom(Random random) { + this.random = random; + } + + /** + * Only used for helping unit test. + */ + @VisibleForTesting + interface TestHelper { + /** + * Used for verify/inspect message received from server. + */ + void onMessage(Message message); + + /** + * Used for verify/inspect error received from server. + */ + void onRpcError(Throwable exception); + } + + @VisibleForTesting + void setTestHelper(TestHelper testHelper) { + this.testHelper = testHelper; + } } diff --git a/examples/src/main/java/io/grpc/examples/routeguide/RouteGuideServer.java b/examples/src/main/java/io/grpc/examples/routeguide/RouteGuideServer.java index 3b7c9971f8..aac4f706a9 100644 --- a/examples/src/main/java/io/grpc/examples/routeguide/RouteGuideServer.java +++ b/examples/src/main/java/io/grpc/examples/routeguide/RouteGuideServer.java @@ -188,7 +188,7 @@ public class RouteGuideServer { int featureCount; int distance; Point previous; - long startTime = System.nanoTime(); + final long startTime = System.nanoTime(); @Override public void onNext(Point point) { @@ -295,7 +295,7 @@ public class RouteGuideServer { double lat2 = RouteGuideUtil.getLatitude(end); double lon1 = RouteGuideUtil.getLongitude(start); double lon2 = RouteGuideUtil.getLongitude(end); - int r = 6371000; // metres + int r = 6371000; // meters double phi1 = toRadians(lat1); double phi2 = toRadians(lat2); double deltaPhi = toRadians(lat2 - lat1); diff --git a/examples/src/test/java/io/grpc/examples/header/HeaderClientInterceptorTest.java b/examples/src/test/java/io/grpc/examples/header/HeaderClientInterceptorTest.java new file mode 100644 index 0000000000..b01c08f167 --- /dev/null +++ b/examples/src/test/java/io/grpc/examples/header/HeaderClientInterceptorTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2016, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.grpc.examples.header; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.Server; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.ServerInterceptors; +import io.grpc.StatusRuntimeException; +import io.grpc.examples.helloworld.GreeterGrpc; +import io.grpc.examples.helloworld.GreeterGrpc.GreeterBlockingStub; +import io.grpc.examples.helloworld.GreeterGrpc.GreeterImplBase; +import io.grpc.examples.helloworld.HelloReply; +import io.grpc.examples.helloworld.HelloRequest; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Matchers; + +/** + * Unit tests for {@link HeaderClientInterceptor}. + * For demonstrating how to write gRPC unit test only. + * Not intended to provide a high code coverage or to test every major usecase. + * + *

For basic unit test examples see {@link io.grpc.examples.helloworld.HelloWorldClientTest} and + * {@link io.grpc.examples.helloworld.HelloWorldServerTest}. + */ +@RunWith(JUnit4.class) +public class HeaderClientInterceptorTest { + + private final ServerInterceptor mockServerInterceptor = spy( + new ServerInterceptor() { + @Override + public Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + return next.startCall(call, headers); + } + }); + + private Server fakeServer; + private ManagedChannel inProcessChannel; + + @Before + public void setUp() throws Exception { + String uniqueServerName = "fake server for " + getClass(); + fakeServer = InProcessServerBuilder.forName(uniqueServerName) + .addService(ServerInterceptors.intercept(new GreeterImplBase() {}, mockServerInterceptor)) + .directExecutor() + .build() + .start(); + inProcessChannel = InProcessChannelBuilder.forName(uniqueServerName) + .intercept(new HeaderClientInterceptor()) + .directExecutor() + .build(); + } + + @After + public void tearDown() { + inProcessChannel.shutdownNow(); + fakeServer.shutdownNow(); + } + + @Test + public void clientHeaderDeliveredToServer() { + GreeterBlockingStub blockingStub = GreeterGrpc.newBlockingStub(inProcessChannel); + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(Metadata.class); + + try { + blockingStub.sayHello(HelloRequest.getDefaultInstance()); + fail(); + } catch (StatusRuntimeException expected) { + // expected because the method is not implemented at server side + } + + verify(mockServerInterceptor).interceptCall( + Matchers.>any(), + metadataCaptor.capture(), + Matchers.>any()); + assertEquals( + "customRequestValue", + metadataCaptor.getValue().get(HeaderClientInterceptor.CUSTOM_HEADER_KEY)); + } +} diff --git a/examples/src/test/java/io/grpc/examples/header/HeaderServerInterceptorTest.java b/examples/src/test/java/io/grpc/examples/header/HeaderServerInterceptorTest.java new file mode 100644 index 0000000000..e1c8a1e8a4 --- /dev/null +++ b/examples/src/test/java/io/grpc/examples/header/HeaderServerInterceptorTest.java @@ -0,0 +1,134 @@ +/* + * Copyright 2016, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.grpc.examples.header; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Server; +import io.grpc.ServerInterceptors; +import io.grpc.examples.helloworld.GreeterGrpc; +import io.grpc.examples.helloworld.GreeterGrpc.GreeterBlockingStub; +import io.grpc.examples.helloworld.GreeterGrpc.GreeterImplBase; +import io.grpc.examples.helloworld.HelloReply; +import io.grpc.examples.helloworld.HelloRequest; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; + +/** + * Unit tests for {@link HeaderClientInterceptor}. + * For demonstrating how to write gRPC unit test only. + * Not intended to provide a high code coverage or to test every major usecase. + * + *

For basic unit test examples see {@link io.grpc.examples.helloworld.HelloWorldClientTest} and + * {@link io.grpc.examples.helloworld.HelloWorldServerTest}. + */ +@RunWith(JUnit4.class) +public class HeaderServerInterceptorTest { + private Server fakeServer; + private ManagedChannel inProcessChannel; + + @Before + public void setUp() throws Exception { + String uniqueServerName = "fake server for " + getClass(); + GreeterImplBase greeterImplBase = + new GreeterImplBase() { + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + responseObserver.onNext(HelloReply.getDefaultInstance()); + responseObserver.onCompleted(); + } + }; + fakeServer = InProcessServerBuilder.forName(uniqueServerName) + .addService(ServerInterceptors.intercept(greeterImplBase, new HeaderServerInterceptor())) + .directExecutor() + .build() + .start(); + inProcessChannel = InProcessChannelBuilder.forName(uniqueServerName).directExecutor().build(); + } + + @After + public void tearDown() { + inProcessChannel.shutdownNow(); + fakeServer.shutdownNow(); + } + + @Test + public void serverHeaderDeliveredToClient() { + class SpyingClientInterceptor implements ClientInterceptor { + ClientCall.Listener spyListener; + + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new SimpleForwardingClientCall(next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + spyListener = responseListener = spy(responseListener); + super.start(responseListener, headers); + } + }; + } + } + + SpyingClientInterceptor clientInterceptor = new SpyingClientInterceptor(); + GreeterBlockingStub blockingStub = + GreeterGrpc.newBlockingStub(inProcessChannel).withInterceptors(clientInterceptor); + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(Metadata.class); + + blockingStub.sayHello(HelloRequest.getDefaultInstance()); + + assertNotNull(clientInterceptor.spyListener); + verify(clientInterceptor.spyListener).onHeaders(metadataCaptor.capture()); + assertEquals( + "customRespondValue", + metadataCaptor.getValue().get(HeaderServerInterceptor.CUSTOM_HEADER_KEY)); + } +} diff --git a/examples/src/test/java/io/grpc/examples/helloworld/HelloWorldClientTest.java b/examples/src/test/java/io/grpc/examples/helloworld/HelloWorldClientTest.java new file mode 100644 index 0000000000..d0614f28fa --- /dev/null +++ b/examples/src/test/java/io/grpc/examples/helloworld/HelloWorldClientTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2015, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.grpc.examples.helloworld; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import io.grpc.ManagedChannelBuilder; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Matchers; + +/** + * Unit tests for {@link HelloWorldClient}. + * For demonstrating how to write gRPC unit test only. + * Not intended to provide a high code coverage or to test every major usecase. + * + *

For more unit test examples see {@link io.grpc.examples.routeguide.RouteGuideClientTest} and + * {@link io.grpc.examples.routeguide.RouteGuideServerTest}. + */ +@RunWith(JUnit4.class) +public class HelloWorldClientTest { + private final GreeterGrpc.GreeterImplBase serviceImpl = spy(new GreeterGrpc.GreeterImplBase() {}); + + private Server fakeServer; + private HelloWorldClient client; + + /** + * Creates and starts a fake in-process server, and creates a client with an in-process channel. + */ + @Before + public void setUp() throws Exception { + String uniqueServerName = "fake server for " + getClass(); + fakeServer = InProcessServerBuilder + .forName(uniqueServerName).directExecutor().addService(serviceImpl).build().start(); + ManagedChannelBuilder channelBuilder = + InProcessChannelBuilder.forName(uniqueServerName).directExecutor(); + client = new HelloWorldClient(channelBuilder); + } + + /** + * Shuts down the client and server. + */ + @After + public void tearDown() throws Exception { + client.shutdown(); + fakeServer.shutdownNow(); + } + + /** + * To test the client, call from the client against the fake server, and verify behaviors or state + * changes from the server side. + */ + @Test + public void greet_messageDeliveredToServer() { + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HelloRequest.class); + String testName = "test name"; + + client.greet(testName); + + verify(serviceImpl) + .sayHello(requestCaptor.capture(), Matchers.>any()); + assertEquals(testName, requestCaptor.getValue().getName()); + } +} diff --git a/examples/src/test/java/io/grpc/examples/helloworld/HelloWorldServerTest.java b/examples/src/test/java/io/grpc/examples/helloworld/HelloWorldServerTest.java new file mode 100644 index 0000000000..9f51258fea --- /dev/null +++ b/examples/src/test/java/io/grpc/examples/helloworld/HelloWorldServerTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.grpc.examples.helloworld; + +import static org.junit.Assert.assertEquals; + +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.examples.helloworld.HelloWorldServer.GreeterImpl; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Unit tests for {@link HelloWorldServer}. + * For demonstrating how to write gRPC unit test only. + * Not intended to provide a high code coverage or to test every major usecase. + * + *

For more unit test examples see {@link io.grpc.examples.routeguide.RouteGuideClientTest} and + * {@link io.grpc.examples.routeguide.RouteGuideServerTest}. + */ +@RunWith(JUnit4.class) +public class HelloWorldServerTest { + private static final String UNIQUE_SERVER_NAME = + "in-process server for " + HelloWorldServerTest.class; + private final Server inProcessServer = InProcessServerBuilder + .forName(UNIQUE_SERVER_NAME).addService(new GreeterImpl()).directExecutor().build(); + private final ManagedChannel inProcessChannel = + InProcessChannelBuilder.forName(UNIQUE_SERVER_NAME).directExecutor().build(); + + /** + * Creates and starts the server with the {@link InProcessServerBuilder}, + * and creates an in-process channel with the {@link InProcessChannelBuilder}. + */ + @Before + public void setUp() throws Exception { + inProcessServer.start(); + } + + /** + * Shuts down the in-process channel and server. + */ + @After + public void tearDown() { + inProcessChannel.shutdownNow(); + inProcessServer.shutdownNow(); + } + + /** + * To test the server, make calls with a real stub using the in-process channel, and verify + * behaviors or state changes from the client side. + */ + @Test + public void greeterImpl_replyMessage() throws Exception { + GreeterGrpc.GreeterBlockingStub blockingStub = GreeterGrpc.newBlockingStub(inProcessChannel); + String testName = "test name"; + + HelloReply reply = blockingStub.sayHello(HelloRequest.newBuilder().setName(testName).build()); + + assertEquals("Hello " + testName, reply.getMessage()); + } +} diff --git a/examples/src/test/java/io/grpc/examples/routeguide/RouteGuideClientTest.java b/examples/src/test/java/io/grpc/examples/routeguide/RouteGuideClientTest.java new file mode 100644 index 0000000000..c5a61ea778 --- /dev/null +++ b/examples/src/test/java/io/grpc/examples/routeguide/RouteGuideClientTest.java @@ -0,0 +1,577 @@ +/* + * Copyright 2016, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.grpc.examples.routeguide; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import com.google.protobuf.Message; + +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.examples.routeguide.RouteGuideClient.TestHelper; +import io.grpc.examples.routeguide.RouteGuideGrpc.RouteGuideImplBase; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import io.grpc.util.MutableHandlerRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Unit tests for {@link RouteGuideClient}. + * For demonstrating how to write gRPC unit test only. + * Not intended to provide a high code coverage or to test every major usecase. + * + *

For basic unit test examples see {@link io.grpc.examples.helloworld.HelloWorldClientTest} and + * {@link io.grpc.examples.helloworld.HelloWorldServerTest}. + */ +@RunWith(JUnit4.class) +public class RouteGuideClientTest { + private final MutableHandlerRegistry serviceRegistry = new MutableHandlerRegistry(); + private final TestHelper testHelper = mock(TestHelper.class); + private final Random noRandomness = + new Random() { + int index; + boolean isForSleep; + + /** + * Returns a number deterministically. If the random number is for sleep time, then return + * -500 so that {@code Thread.sleep(random.nextInt(1000) + 500)} sleeps 0 ms. Otherwise, it + * is for list index, then return incrementally (and cyclically). + */ + @Override + public int nextInt(int bound) { + int retVal = isForSleep ? -500 : (index++ % bound); + isForSleep = ! isForSleep; + return retVal; + } + }; + private Server fakeServer; + private RouteGuideClient client; + + @Before + public void setUp() throws Exception { + String uniqueServerName = "fake server for " + getClass(); + + // use a mutable service registry for later registering the service impl for each test case. + fakeServer = InProcessServerBuilder.forName(uniqueServerName) + .fallbackHandlerRegistry(serviceRegistry).directExecutor().build().start(); + client = + new RouteGuideClient(InProcessChannelBuilder.forName(uniqueServerName).directExecutor()); + client.setTestHelper(testHelper); + } + + @After + public void tearDown() throws Exception { + client.shutdown(); + fakeServer.shutdownNow(); + } + + /** + * Example for testing blocking unary call. + */ + @Test + public void getFeature() { + Point requestPoint = Point.newBuilder().setLatitude(-1).setLongitude(-1).build(); + Point responsePoint = Point.newBuilder().setLatitude(-123).setLongitude(-123).build(); + final AtomicReference pointDelivered = new AtomicReference(); + final Feature responseFeature = + Feature.newBuilder().setName("dummyFeature").setLocation(responsePoint).build(); + + // implement the fake service + RouteGuideImplBase getFeatureImpl = + new RouteGuideImplBase() { + @Override + public void getFeature(Point point, StreamObserver responseObserver) { + pointDelivered.set(point); + responseObserver.onNext(responseFeature); + responseObserver.onCompleted(); + } + }; + serviceRegistry.addService(getFeatureImpl); + + client.getFeature(-1, -1); + + assertEquals(requestPoint, pointDelivered.get()); + verify(testHelper).onMessage(responseFeature); + verify(testHelper, never()).onRpcError(any(Throwable.class)); + } + + /** + * Example for testing blocking unary call. + */ + @Test + public void getFeature_error() { + Point requestPoint = Point.newBuilder().setLatitude(-1).setLongitude(-1).build(); + final AtomicReference pointDelivered = new AtomicReference(); + final StatusRuntimeException fakeError = new StatusRuntimeException(Status.DATA_LOSS); + + // implement the fake service + RouteGuideImplBase getFeatureImpl = + new RouteGuideImplBase() { + @Override + public void getFeature(Point point, StreamObserver responseObserver) { + pointDelivered.set(point); + responseObserver.onError(fakeError); + } + }; + serviceRegistry.addService(getFeatureImpl); + + client.getFeature(-1, -1); + + assertEquals(requestPoint, pointDelivered.get()); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(testHelper).onRpcError(errorCaptor.capture()); + assertEquals(fakeError.getStatus(), Status.fromThrowable(errorCaptor.getValue())); + } + + /** + * Example for testing blocking server-streaming. + */ + @Test + public void listFeatures() { + final Feature responseFeature1 = Feature.newBuilder().setName("feature 1").build(); + final Feature responseFeature2 = Feature.newBuilder().setName("feature 2").build(); + final AtomicReference rectangleDelivered = new AtomicReference(); + + // implement the fake service + RouteGuideImplBase listFeaturesImpl = + new RouteGuideImplBase() { + @Override + public void listFeatures(Rectangle rectangle, StreamObserver responseObserver) { + rectangleDelivered.set(rectangle); + + // send two response messages + responseObserver.onNext(responseFeature1); + responseObserver.onNext(responseFeature2); + + // complete the response + responseObserver.onCompleted(); + } + }; + serviceRegistry.addService(listFeaturesImpl); + + client.listFeatures(1, 2, 3, 4); + + assertEquals(Rectangle.newBuilder() + .setLo(Point.newBuilder().setLatitude(1).setLongitude(2).build()) + .setHi(Point.newBuilder().setLatitude(3).setLongitude(4).build()) + .build(), + rectangleDelivered.get()); + verify(testHelper).onMessage(responseFeature1); + verify(testHelper).onMessage(responseFeature2); + verify(testHelper, never()).onRpcError(any(Throwable.class)); + } + + /** + * Example for testing blocking server-streaming. + */ + @Test + public void listFeatures_error() { + final Feature responseFeature1 = + Feature.newBuilder().setName("feature 1").build(); + final AtomicReference rectangleDelivered = new AtomicReference(); + final StatusRuntimeException fakeError = new StatusRuntimeException(Status.INVALID_ARGUMENT); + + // implement the fake service + RouteGuideImplBase listFeaturesImpl = + new RouteGuideImplBase() { + @Override + public void listFeatures(Rectangle rectangle, StreamObserver responseObserver) { + rectangleDelivered.set(rectangle); + + // send one response message + responseObserver.onNext(responseFeature1); + + // let the rpc fail + responseObserver.onError(fakeError); + } + }; + serviceRegistry.addService(listFeaturesImpl); + + client.listFeatures(1, 2, 3, 4); + + assertEquals(Rectangle.newBuilder() + .setLo(Point.newBuilder().setLatitude(1).setLongitude(2).build()) + .setHi(Point.newBuilder().setLatitude(3).setLongitude(4).build()) + .build(), + rectangleDelivered.get()); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(testHelper).onMessage(responseFeature1); + verify(testHelper).onRpcError(errorCaptor.capture()); + assertEquals(fakeError.getStatus(), Status.fromThrowable(errorCaptor.getValue())); + } + + /** + * Example for testing async client-streaming. + */ + @Test + public void recordRoute() throws Exception { + client.setRandom(noRandomness); + Point point1 = Point.newBuilder().setLatitude(1).setLongitude(1).build(); + Point point2 = Point.newBuilder().setLatitude(2).setLongitude(2).build(); + Point point3 = Point.newBuilder().setLatitude(3).setLongitude(3).build(); + Feature requestFeature1 = + Feature.newBuilder().setLocation(point1).build(); + Feature requestFeature2 = + Feature.newBuilder().setLocation(point2).build(); + Feature requestFeature3 = + Feature.newBuilder().setLocation(point3).build(); + final List features = Arrays.asList( + requestFeature1, requestFeature2, requestFeature3); + final List pointsDelivered = new ArrayList(); + final RouteSummary fakeResponse = RouteSummary + .newBuilder() + .setPointCount(7) + .setFeatureCount(8) + .setDistance(9) + .setElapsedTime(10) + .build(); + + // implement the fake service + RouteGuideImplBase recordRouteImpl = + new RouteGuideImplBase() { + @Override + public StreamObserver recordRoute( + final StreamObserver responseObserver) { + StreamObserver requestObserver = new StreamObserver() { + @Override + public void onNext(Point value) { + pointsDelivered.add(value); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + responseObserver.onNext(fakeResponse); + responseObserver.onCompleted(); + } + }; + + return requestObserver; + } + }; + serviceRegistry.addService(recordRouteImpl); + + // send requestFeature1, requestFeature2, requestFeature3, and then requestFeature1 again + client.recordRoute(features, 4); + + assertEquals( + Arrays.asList( + requestFeature1.getLocation(), + requestFeature2.getLocation(), + requestFeature3.getLocation(), + requestFeature1.getLocation()), + pointsDelivered); + verify(testHelper).onMessage(fakeResponse); + verify(testHelper, never()).onRpcError(any(Throwable.class)); + } + + /** + * Example for testing async client-streaming. + */ + @Test + public void recordRoute_wrongResponse() throws Exception { + client.setRandom(noRandomness); + Point point1 = Point.newBuilder().setLatitude(1).setLongitude(1).build(); + final Feature requestFeature1 = + Feature.newBuilder().setLocation(point1).build(); + final List features = Arrays.asList(requestFeature1); + + // implement the fake service + RouteGuideImplBase recordRouteImpl = + new RouteGuideImplBase() { + @Override + public StreamObserver recordRoute(StreamObserver responseObserver) { + RouteSummary response = RouteSummary.getDefaultInstance(); + // sending more than one responses is not right for client-streaming call. + responseObserver.onNext(response); + responseObserver.onNext(response); + responseObserver.onCompleted(); + + return new StreamObserver() { + @Override + public void onNext(Point value) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + } + }; + } + }; + serviceRegistry.addService(recordRouteImpl); + + client.recordRoute(features, 4); + + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(testHelper).onRpcError(errorCaptor.capture()); + assertEquals(Status.Code.CANCELLED, Status.fromThrowable(errorCaptor.getValue()).getCode()); + } + + /** + * Example for testing async client-streaming. + */ + @Test + public void recordRoute_serverError() throws Exception { + client.setRandom(noRandomness); + Point point1 = Point.newBuilder().setLatitude(1).setLongitude(1).build(); + final Feature requestFeature1 = + Feature.newBuilder().setLocation(point1).build(); + final List features = Arrays.asList(requestFeature1); + final StatusRuntimeException fakeError = new StatusRuntimeException(Status.INVALID_ARGUMENT); + + // implement the fake service + RouteGuideImplBase recordRouteImpl = + new RouteGuideImplBase() { + @Override + public StreamObserver recordRoute(StreamObserver responseObserver) { + // send an error immediately + responseObserver.onError(fakeError); + + StreamObserver requestObserver = new StreamObserver() { + @Override + public void onNext(Point value) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + } + }; + return requestObserver; + } + }; + serviceRegistry.addService(recordRouteImpl); + + client.recordRoute(features, 4); + + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(testHelper).onRpcError(errorCaptor.capture()); + assertEquals(fakeError.getStatus(), Status.fromThrowable(errorCaptor.getValue())); + } + + /** + * Example for testing bi-directional call. + */ + @Test + public void routeChat_simpleResponse() throws Exception { + RouteNote fakeResponse1 = RouteNote.newBuilder().setMessage("dummy msg1").build(); + RouteNote fakeResponse2 = RouteNote.newBuilder().setMessage("dummy msg2").build(); + final List messagesDelivered = new ArrayList(); + final List locationsDelivered = new ArrayList(); + final AtomicReference> responseObserverRef = + new AtomicReference>(); + final CountDownLatch allRequestsDelivered = new CountDownLatch(1); + // implement the fake service + RouteGuideImplBase routeChatImpl = + new RouteGuideImplBase() { + @Override + public StreamObserver routeChat(StreamObserver responseObserver) { + responseObserverRef.set(responseObserver); + + StreamObserver requestObserver = new StreamObserver() { + @Override + public void onNext(RouteNote value) { + messagesDelivered.add(value.getMessage()); + locationsDelivered.add(value.getLocation()); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + allRequestsDelivered.countDown(); + } + }; + + return requestObserver; + } + }; + serviceRegistry.addService(routeChatImpl); + + // start routeChat + CountDownLatch latch = client.routeChat(); + + // request message sent and delivered for four times + assertTrue(allRequestsDelivered.await(1, TimeUnit.SECONDS)); + assertEquals( + Arrays.asList("First message", "Second message", "Third message", "Fourth message"), + messagesDelivered); + assertEquals( + Arrays.asList( + Point.newBuilder().setLatitude(0).setLongitude(0).build(), + Point.newBuilder().setLatitude(0).setLongitude(1).build(), + Point.newBuilder().setLatitude(1).setLongitude(0).build(), + Point.newBuilder().setLatitude(1).setLongitude(1).build() + ), + locationsDelivered); + + // Let the server send out two simple response messages + // and verify that the client receives them. + // Allow some timeout for verify() if not using directExecutor + responseObserverRef.get().onNext(fakeResponse1); + verify(testHelper).onMessage(fakeResponse1); + responseObserverRef.get().onNext(fakeResponse2); + verify(testHelper).onMessage(fakeResponse2); + + // let server complete. + responseObserverRef.get().onCompleted(); + + assertTrue(latch.await(1, TimeUnit.SECONDS)); + verify(testHelper, never()).onRpcError(any(Throwable.class)); + } + + /** + * Example for testing bi-directional call. + */ + @Test + public void routeChat_echoResponse() throws Exception { + final List notesDelivered = new ArrayList(); + + // implement the fake service + RouteGuideImplBase routeChatImpl = + new RouteGuideImplBase() { + @Override + public StreamObserver routeChat( + final StreamObserver responseObserver) { + StreamObserver requestObserver = new StreamObserver() { + @Override + public void onNext(RouteNote value) { + notesDelivered.add(value); + responseObserver.onNext(value); + } + + @Override + public void onError(Throwable t) { + responseObserver.onError(t); + } + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + + return requestObserver; + } + }; + serviceRegistry.addService(routeChatImpl); + + client.routeChat().await(1, TimeUnit.SECONDS); + + String[] messages = + {"First message", "Second message", "Third message", "Fourth message"}; + for (int i = 0; i < 4; i++) { + verify(testHelper).onMessage(notesDelivered.get(i)); + assertEquals(messages[i], notesDelivered.get(i).getMessage()); + } + + verify(testHelper, never()).onRpcError(any(Throwable.class)); + } + + /** + * Example for testing bi-directional call. + */ + @Test + public void routeChat_errorResponse() throws Exception { + final List notesDelivered = new ArrayList(); + final StatusRuntimeException fakeError = new StatusRuntimeException(Status.PERMISSION_DENIED); + + // implement the fake service + RouteGuideImplBase routeChatImpl = + new RouteGuideImplBase() { + @Override + public StreamObserver routeChat( + final StreamObserver responseObserver) { + StreamObserver requestObserver = new StreamObserver() { + @Override + public void onNext(RouteNote value) { + notesDelivered.add(value); + responseObserver.onError(fakeError); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + + return requestObserver; + } + }; + serviceRegistry.addService(routeChatImpl); + + client.routeChat().await(1, TimeUnit.SECONDS); + + assertEquals("First message", notesDelivered.get(0).getMessage()); + verify(testHelper, never()).onMessage(any(Message.class)); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(testHelper).onRpcError(errorCaptor.capture()); + assertEquals(fakeError.getStatus(), Status.fromThrowable(errorCaptor.getValue())); + } +} diff --git a/examples/src/test/java/io/grpc/examples/routeguide/RouteGuideServerTest.java b/examples/src/test/java/io/grpc/examples/routeguide/RouteGuideServerTest.java new file mode 100644 index 0000000000..3197776505 --- /dev/null +++ b/examples/src/test/java/io/grpc/examples/routeguide/RouteGuideServerTest.java @@ -0,0 +1,282 @@ +/* + * Copyright 2016, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.grpc.examples.routeguide; + +import static junit.framework.TestCase.fail; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import io.grpc.ManagedChannel; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Unit tests for {@link RouteGuideServer}. + * For demonstrating how to write gRPC unit test only. + * Not intended to provide a high code coverage or to test every major usecase. + * + *

For basic unit test examples see {@link io.grpc.examples.helloworld.HelloWorldClientTest} and + * {@link io.grpc.examples.helloworld.HelloWorldServerTest}. + */ +@RunWith(JUnit4.class) +public class RouteGuideServerTest { + private RouteGuideServer server; + private ManagedChannel inProcessChannel; + private Collection features; + + @Before + public void setUp() throws Exception { + String uniqueServerName = "in-process server for " + getClass(); + features = new ArrayList(); + // use directExecutor for both InProcessServerBuilder and InProcessChannelBuilder can reduce the + // usage timeouts and latches in test. But we still add timeout and latches where they would be + // needed if no directExecutor were used, just for demo purpose. + server = new RouteGuideServer( + InProcessServerBuilder.forName(uniqueServerName).directExecutor(), 0, features); + server.start(); + inProcessChannel = InProcessChannelBuilder.forName(uniqueServerName).directExecutor().build(); + } + + @After + public void tearDown() throws Exception { + inProcessChannel.shutdownNow(); + server.stop(); + } + + @Test + public void getFeature() { + Point point = Point.newBuilder().setLongitude(1).setLatitude(1).build(); + Feature unnamedFeature = Feature.newBuilder() + .setName("").setLocation(point).build(); + RouteGuideGrpc.RouteGuideBlockingStub stub = RouteGuideGrpc.newBlockingStub(inProcessChannel); + + // feature not found in the server + Feature feature = stub.getFeature(point); + + assertEquals(unnamedFeature, feature); + + // feature found in the server + Feature namedFeature = Feature.newBuilder() + .setName("name").setLocation(point).build(); + features.add(namedFeature); + + feature = stub.getFeature(point); + + assertEquals(namedFeature, feature); + } + + @Test + public void listFeatures() throws Exception { + // setup + Rectangle rect = Rectangle.newBuilder() + .setLo(Point.newBuilder().setLongitude(0).setLatitude(0).build()) + .setHi(Point.newBuilder().setLongitude(10).setLatitude(10).build()) + .build(); + Feature f1 = Feature.newBuilder() + .setLocation(Point.newBuilder().setLongitude(-1).setLatitude(-1).build()) + .setName("f1") + .build(); // not inside rect + Feature f2 = Feature.newBuilder() + .setLocation(Point.newBuilder().setLongitude(2).setLatitude(2).build()) + .setName("f2") + .build(); + Feature f3 = Feature.newBuilder() + .setLocation(Point.newBuilder().setLongitude(3).setLatitude(3).build()) + .setName("f3") + .build(); + Feature f4 = Feature.newBuilder() + .setLocation(Point.newBuilder().setLongitude(4).setLatitude(4).build()) + .build(); // unamed + features.add(f1); + features.add(f2); + features.add(f3); + features.add(f4); + final Collection result = new HashSet(); + final CountDownLatch latch = new CountDownLatch(1); + StreamObserver responseObserver = + new StreamObserver() { + @Override + public void onNext(Feature value) { + result.add(value); + } + + @Override + public void onError(Throwable t) { + fail(); + } + + @Override + public void onCompleted() { + latch.countDown(); + } + }; + RouteGuideGrpc.RouteGuideStub stub = RouteGuideGrpc.newStub(inProcessChannel); + + // run + stub.listFeatures(rect, responseObserver); + assertTrue(latch.await(1, TimeUnit.SECONDS)); + + // verify + assertEquals(new HashSet(Arrays.asList(f2, f3)), result); + } + + @Test + public void recordRoute() { + Point p1 = Point.newBuilder().setLongitude(1000).setLatitude(1000).build(); + Point p2 = Point.newBuilder().setLongitude(2000).setLatitude(2000).build(); + Point p3 = Point.newBuilder().setLongitude(3000).setLatitude(3000).build(); + Point p4 = Point.newBuilder().setLongitude(4000).setLatitude(4000).build(); + Feature f1 = Feature.newBuilder().setLocation(p1).build(); // unamed + Feature f2 = Feature.newBuilder().setLocation(p2).setName("f2").build(); + Feature f3 = Feature.newBuilder().setLocation(p3).setName("f3").build(); + Feature f4 = Feature.newBuilder().setLocation(p4).build(); // unamed + features.add(f1); + features.add(f2); + features.add(f3); + features.add(f4); + + @SuppressWarnings("unchecked") + StreamObserver responseObserver = + (StreamObserver) mock(StreamObserver.class); + RouteGuideGrpc.RouteGuideStub stub = RouteGuideGrpc.newStub(inProcessChannel); + ArgumentCaptor routeSummaryCaptor = ArgumentCaptor.forClass(RouteSummary.class); + + StreamObserver requestObserver = stub.recordRoute(responseObserver); + + requestObserver.onNext(p1); + requestObserver.onNext(p2); + requestObserver.onNext(p3); + requestObserver.onNext(p4); + + verify(responseObserver, never()).onNext(any(RouteSummary.class)); + + requestObserver.onCompleted(); + + // allow some ms to let client receive the response. Similar usage later on. + verify(responseObserver, timeout(100)).onNext(routeSummaryCaptor.capture()); + RouteSummary summary = routeSummaryCaptor.getValue(); + assertEquals(45, summary.getDistance()); // 45 is the hard coded distance from p1 to p4. + assertEquals(2, summary.getFeatureCount()); + verify(responseObserver, timeout(100)).onCompleted(); + verify(responseObserver, never()).onError(any(Throwable.class)); + } + + @Test + public void routeChat() { + Point p1 = Point.newBuilder().setLongitude(1).setLatitude(1).build(); + Point p2 = Point.newBuilder().setLongitude(2).setLatitude(2).build(); + RouteNote n1 = RouteNote.newBuilder().setLocation(p1).setMessage("m1").build(); + RouteNote n2 = RouteNote.newBuilder().setLocation(p2).setMessage("m2").build(); + RouteNote n3 = RouteNote.newBuilder().setLocation(p1).setMessage("m3").build(); + RouteNote n4 = RouteNote.newBuilder().setLocation(p2).setMessage("m4").build(); + RouteNote n5 = RouteNote.newBuilder().setLocation(p1).setMessage("m5").build(); + RouteNote n6 = RouteNote.newBuilder().setLocation(p1).setMessage("m6").build(); + int timesOnNext = 0; + + @SuppressWarnings("unchecked") + StreamObserver responseObserver = + (StreamObserver) mock(StreamObserver.class); + RouteGuideGrpc.RouteGuideStub stub = RouteGuideGrpc.newStub(inProcessChannel); + + StreamObserver requestObserver = stub.routeChat(responseObserver); + verify(responseObserver, never()).onNext(any(RouteNote.class)); + + requestObserver.onNext(n1); + verify(responseObserver, never()).onNext(any(RouteNote.class)); + + requestObserver.onNext(n2); + verify(responseObserver, never()).onNext(any(RouteNote.class)); + + requestObserver.onNext(n3); + ArgumentCaptor routeNoteCaptor = ArgumentCaptor.forClass(RouteNote.class); + verify(responseObserver, timeout(100).times(++timesOnNext)).onNext(routeNoteCaptor.capture()); + RouteNote result = routeNoteCaptor.getValue(); + assertEquals(p1, result.getLocation()); + assertEquals("m1", result.getMessage()); + + requestObserver.onNext(n4); + routeNoteCaptor = ArgumentCaptor.forClass(RouteNote.class); + verify(responseObserver, timeout(100).times(++timesOnNext)).onNext(routeNoteCaptor.capture()); + result = routeNoteCaptor.getAllValues().get(timesOnNext - 1); + assertEquals(p2, result.getLocation()); + assertEquals("m2", result.getMessage()); + + requestObserver.onNext(n5); + routeNoteCaptor = ArgumentCaptor.forClass(RouteNote.class); + timesOnNext += 2; + verify(responseObserver, timeout(100).times(timesOnNext)).onNext(routeNoteCaptor.capture()); + result = routeNoteCaptor.getAllValues().get(timesOnNext - 2); + assertEquals(p1, result.getLocation()); + assertEquals("m1", result.getMessage()); + result = routeNoteCaptor.getAllValues().get(timesOnNext - 1); + assertEquals(p1, result.getLocation()); + assertEquals("m3", result.getMessage()); + + requestObserver.onNext(n6); + routeNoteCaptor = ArgumentCaptor.forClass(RouteNote.class); + timesOnNext += 3; + verify(responseObserver, timeout(100).times(timesOnNext)).onNext(routeNoteCaptor.capture()); + result = routeNoteCaptor.getAllValues().get(timesOnNext - 3); + assertEquals(p1, result.getLocation()); + assertEquals("m1", result.getMessage()); + result = routeNoteCaptor.getAllValues().get(timesOnNext - 2); + assertEquals(p1, result.getLocation()); + assertEquals("m3", result.getMessage()); + result = routeNoteCaptor.getAllValues().get(timesOnNext - 1); + assertEquals(p1, result.getLocation()); + assertEquals("m5", result.getMessage()); + + requestObserver.onCompleted(); + verify(responseObserver, timeout(100)).onCompleted(); + verify(responseObserver, never()).onError(any(Throwable.class)); + } +}