testing: added junit rule for in-process servers

GrpcServerRule configures an in-process server and channel. It is
useful for asserting requests being made to a service. A consumer can
create a mock implementation of their service that records each
request, then make assertions on those records in their test.
This commit is contained in:
Joey Bratton 2016-12-06 12:32:04 -05:00 committed by Eric Anderson
parent a10261af48
commit b6ebede94f
18 changed files with 446 additions and 26 deletions

View File

@ -3,16 +3,6 @@ apply plugin: 'application'
description = "gRPC: Integration Testing"
startScripts.enabled = false
// Add dependency on the protobuf plugin
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath libraries.protobuf_plugin
}
}
dependencies {
compile project(':grpc-auth'),
project(':grpc-core'),
@ -21,6 +11,7 @@ dependencies {
project(':grpc-protobuf'),
project(':grpc-stub'),
project(':grpc-testing'),
project(':grpc-testing-proto'),
libraries.junit,
libraries.mockito,
libraries.netty_tcnative,
@ -84,13 +75,3 @@ applicationDistribution.into("bin") {
from(stresstest_client)
fileMode = 0755
}
configureProtoCompilation()
// Let intellij projects refer to generated code
idea {
module {
sourceDirs += file("${projectDir}/src/generated/main/java");
sourceDirs += file("${projectDir}/src/generated/main/grpc");
}
}

View File

@ -10,6 +10,7 @@ include ":grpc-protobuf-nano"
include ":grpc-netty"
include ":grpc-grpclb"
include ":grpc-testing"
include ":grpc-testing-proto"
include ":grpc-interop-testing"
include ":grpc-all"
include ":grpc-benchmarks"
@ -27,6 +28,7 @@ project(':grpc-protobuf-nano').projectDir = "$rootDir/protobuf-nano" as File
project(':grpc-netty').projectDir = "$rootDir/netty" as File
project(':grpc-grpclb').projectDir = "$rootDir/grpclb" as File
project(':grpc-testing').projectDir = "$rootDir/testing" as File
project(':grpc-testing-proto').projectDir = "$rootDir/testing-proto" as File
project(':grpc-interop-testing').projectDir = "$rootDir/interop-testing" as File
project(':grpc-all').projectDir = "$rootDir/all" as File
project(':grpc-benchmarks').projectDir = "$rootDir/benchmarks" as File

View File

@ -0,0 +1,26 @@
description = "gRPC: Testing Protos"
// Add dependency on the protobuf plugin
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath libraries.protobuf_plugin
}
}
dependencies {
compile project(':grpc-protobuf'),
project(':grpc-stub')
}
configureProtoCompilation()
// Let intellij projects refer to generated code
idea {
module {
sourceDirs += file("${projectDir}/src/generated/main/java")
sourceDirs += file("${projectDir}/src/generated/main/grpc")
}
}

View File

@ -1,4 +1,3 @@
// Copyright 2015, Google Inc.
// All rights reserved.
//
@ -43,4 +42,4 @@ option java_outer_classname = "EmptyProtos";
// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { };
// };
//
message Empty {}
message Empty {}

View File

@ -1,4 +1,3 @@
// Copyright 2015, Google Inc.
// All rights reserved.
//
@ -176,4 +175,4 @@ message ReconnectParams {
message ReconnectInfo {
bool passed = 1;
repeated int32 backoff_ms = 2;
}
}

View File

@ -1,4 +1,3 @@
// Copyright 2015, Google Inc.
// All rights reserved.
//
@ -80,7 +79,7 @@ service TestService {
// that case.
service UnimplementedService {
// A call that no server should implement
rpc UnimplementedCall(grpc.testing.Empty) returns(grpc.testing.Empty);
rpc UnimplementedCall(grpc.testing.Empty) returns(grpc.testing.Empty);
}
// A service used to control reconnect server.

View File

@ -1,8 +1,11 @@
description = "gRPC: Testing"
dependencies {
compile project(':grpc-core'),
project(':grpc-stub'),
libraries.junit,
libraries.mockito,
libraries.truth
testCompile project(':grpc-testing-proto')
}

View File

@ -0,0 +1,157 @@
/*
* 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.testing;
import io.grpc.BindableService;
import io.grpc.ExperimentalApi;
import io.grpc.ManagedChannel;
import io.grpc.Server;
import io.grpc.ServerServiceDefinition;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.stub.AbstractStub;
import io.grpc.util.MutableHandlerRegistry;
import org.junit.rules.ExternalResource;
import org.junit.rules.TestRule;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* {@code GrpcServerRule} is a JUnit {@link TestRule} that starts an in-process gRPC service with
* a {@link MutableHandlerRegistry} for adding services. It is particularly useful for mocking out
* external gRPC-based services and asserting that the expected requests were made.
*
* <p>An {@link AbstractStub} can be created against this service by using the
* {@link ManagedChannel} provided by {@link GrpcServerRule#getChannel()}.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/2488")
public class GrpcServerRule extends ExternalResource {
private ManagedChannel channel;
private Server server;
private String serverName;
private MutableHandlerRegistry serviceRegistry;
private boolean useDirectExecutor;
/**
* Returns {@code this} configured to use a direct executor for the {@link ManagedChannel} and
* {@link Server}.
*/
public final GrpcServerRule directExecutor() {
useDirectExecutor = true;
return this;
}
/**
* Returns a {@link ManagedChannel} connected to this service.
*/
public final ManagedChannel getChannel() {
return channel;
}
/**
* Returns the underlying gRPC {@link Server} for this service.
*/
public final Server getServer() {
return server;
}
/**
* Returns the randomly generated server name for this service.
*/
public final String getServerName() {
return serverName;
}
/**
* Returns the service registry for this service. The registry is used to add service instances
* (e.g. {@link BindableService} or {@link ServerServiceDefinition} to the server.
*/
public final MutableHandlerRegistry getServiceRegistry() {
return serviceRegistry;
}
/**
* After the test has completed, clean up the channel and server.
*/
@Override
protected void after() {
serverName = null;
serviceRegistry = null;
channel.shutdown();
server.shutdown();
try {
channel.awaitTermination(1, TimeUnit.MINUTES);
server.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
channel.shutdownNow();
channel = null;
server.shutdownNow();
server = null;
}
}
/**
* Before the test has started, create the server and channel.
*/
@Override
protected void before() throws Throwable {
serverName = UUID.randomUUID().toString();
serviceRegistry = new MutableHandlerRegistry();
InProcessServerBuilder serverBuilder = InProcessServerBuilder.forName(serverName)
.fallbackHandlerRegistry(serviceRegistry);
if (useDirectExecutor) {
serverBuilder.directExecutor();
}
server = serverBuilder.build().start();
InProcessChannelBuilder channelBuilder = InProcessChannelBuilder.forName(serverName);
if (useDirectExecutor) {
channelBuilder.directExecutor();
}
channel = channelBuilder.build();
}
}

View File

@ -0,0 +1,254 @@
/*
* 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.testing;
import static com.google.common.truth.Truth.assertThat;
import com.google.protobuf.ByteString;
import com.google.protobuf.EmptyProtos;
import io.grpc.ManagedChannel;
import io.grpc.Server;
import io.grpc.stub.StreamObserver;
import io.grpc.testing.integration.Messages;
import io.grpc.testing.integration.TestServiceGrpc;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runners.model.Statement;
import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
public class GrpcServerRuleTest {
public static class WithoutDirectExecutor {
@Rule
public final GrpcServerRule grpcServerRule = new GrpcServerRule();
@Test
public void serverAndChannelAreStarted() {
assertThat(grpcServerRule.getServer().isShutdown()).isFalse();
assertThat(grpcServerRule.getServer().isTerminated()).isFalse();
assertThat(grpcServerRule.getChannel().isShutdown()).isFalse();
assertThat(grpcServerRule.getChannel().isTerminated()).isFalse();
assertThat(grpcServerRule.getServerName()).isNotNull();
assertThat(grpcServerRule.getServiceRegistry()).isNotNull();
}
@Test
public void serverAllowsServicesToBeAddedViaServiceRegistry() {
TestServiceImpl testService = new TestServiceImpl();
grpcServerRule.getServiceRegistry().addService(testService);
TestServiceGrpc.TestServiceBlockingStub stub =
TestServiceGrpc.newBlockingStub(grpcServerRule.getChannel());
Messages.SimpleRequest request1 = Messages.SimpleRequest.newBuilder()
.setPayload(Messages.Payload.newBuilder()
.setBody(ByteString.copyFromUtf8(UUID.randomUUID().toString())))
.build();
Messages.SimpleRequest request2 = Messages.SimpleRequest.newBuilder()
.setPayload(Messages.Payload.newBuilder()
.setBody(ByteString.copyFromUtf8(UUID.randomUUID().toString())))
.build();
stub.unaryCall(request1);
stub.unaryCall(request2);
assertThat(testService.unaryCallRequests)
.containsExactly(request1, request2);
}
@Test
public void serviceIsNotRunOnSameThreadAsTest() {
TestServiceImpl testService = new TestServiceImpl();
grpcServerRule.getServiceRegistry().addService(testService);
TestServiceGrpc.TestServiceBlockingStub stub =
TestServiceGrpc.newBlockingStub(grpcServerRule.getChannel());
// Make a garbage request first due to https://github.com/grpc/grpc-java/issues/2444.
stub.emptyCall(EmptyProtos.Empty.newBuilder().build());
stub.emptyCall(EmptyProtos.Empty.newBuilder().build());
assertThat(testService.lastEmptyCallRequestThread).isNotEqualTo(Thread.currentThread());
}
}
public static class WithDirectExecutor {
@Rule
public final GrpcServerRule grpcServerRule = new GrpcServerRule().directExecutor();
@Test
public void serverAndChannelAreStarted() {
assertThat(grpcServerRule.getServer().isShutdown()).isFalse();
assertThat(grpcServerRule.getServer().isTerminated()).isFalse();
assertThat(grpcServerRule.getChannel().isShutdown()).isFalse();
assertThat(grpcServerRule.getChannel().isTerminated()).isFalse();
assertThat(grpcServerRule.getServerName()).isNotNull();
assertThat(grpcServerRule.getServiceRegistry()).isNotNull();
}
@Test
public void serverAllowsServicesToBeAddedViaServiceRegistry() {
TestServiceImpl testService = new TestServiceImpl();
grpcServerRule.getServiceRegistry().addService(testService);
TestServiceGrpc.TestServiceBlockingStub stub =
TestServiceGrpc.newBlockingStub(grpcServerRule.getChannel());
Messages.SimpleRequest request1 = Messages.SimpleRequest.newBuilder()
.setPayload(Messages.Payload.newBuilder()
.setBody(ByteString.copyFromUtf8(UUID.randomUUID().toString())))
.build();
Messages.SimpleRequest request2 = Messages.SimpleRequest.newBuilder()
.setPayload(Messages.Payload.newBuilder()
.setBody(ByteString.copyFromUtf8(UUID.randomUUID().toString())))
.build();
stub.unaryCall(request1);
stub.unaryCall(request2);
assertThat(testService.unaryCallRequests)
.containsExactly(request1, request2);
}
@Test
public void serviceIsRunOnSameThreadAsTest() {
TestServiceImpl testService = new TestServiceImpl();
grpcServerRule.getServiceRegistry().addService(testService);
TestServiceGrpc.TestServiceBlockingStub stub =
TestServiceGrpc.newBlockingStub(grpcServerRule.getChannel());
// Make a garbage request first due to https://github.com/grpc/grpc-java/issues/2444.
stub.emptyCall(EmptyProtos.Empty.newBuilder().build());
stub.emptyCall(EmptyProtos.Empty.newBuilder().build());
assertThat(testService.lastEmptyCallRequestThread).isEqualTo(Thread.currentThread());
}
}
public static class ResourceCleanup {
@Test
public void serverAndChannelAreShutdownAfterRule() throws Throwable {
GrpcServerRule grpcServerRule = new GrpcServerRule();
// Before the rule has been executed, all of its resources should be null.
assertThat(grpcServerRule.getChannel()).isNull();
assertThat(grpcServerRule.getServer()).isNull();
assertThat(grpcServerRule.getServerName()).isNull();
assertThat(grpcServerRule.getServiceRegistry()).isNull();
// The TestStatement stores the channel and server instances so that we can inspect them after
// the rule cleans up.
TestStatement statement = new TestStatement(grpcServerRule);
grpcServerRule.apply(statement, null).evaluate();
// Ensure that the stored channel and server instances were shut down.
assertThat(statement.channel.isShutdown()).isTrue();
assertThat(statement.server.isShutdown()).isTrue();
// All references to the resources that we created should be set to null.
assertThat(grpcServerRule.getChannel()).isNull();
assertThat(grpcServerRule.getServer()).isNull();
assertThat(grpcServerRule.getServerName()).isNull();
assertThat(grpcServerRule.getServiceRegistry()).isNull();
}
private static class TestStatement extends Statement {
private final GrpcServerRule grpcServerRule;
private ManagedChannel channel;
private Server server;
private TestStatement(GrpcServerRule grpcServerRule) {
this.grpcServerRule = grpcServerRule;
}
@Override
public void evaluate() throws Throwable {
channel = grpcServerRule.getChannel();
server = grpcServerRule.getServer();
}
}
}
private static class TestServiceImpl extends TestServiceGrpc.TestServiceImplBase {
private final Collection<Messages.SimpleRequest> unaryCallRequests =
new ConcurrentLinkedQueue<Messages.SimpleRequest>();
private volatile Thread lastEmptyCallRequestThread;
@Override
public void emptyCall(
EmptyProtos.Empty request,
StreamObserver<EmptyProtos.Empty> responseObserver) {
lastEmptyCallRequestThread = Thread.currentThread();
responseObserver.onNext(EmptyProtos.Empty.newBuilder().build());
responseObserver.onCompleted();
}
@Override
public void unaryCall(
Messages.SimpleRequest request,
StreamObserver<Messages.SimpleResponse> responseObserver) {
unaryCallRequests.add(request);
responseObserver.onNext(Messages.SimpleResponse.newBuilder().build());
responseObserver.onCompleted();
}
}
}