diff --git a/src/objective-c/tests/InteropTests.m b/src/objective-c/tests/InteropTests.m
new file mode 100644
index 0000000000000000000000000000000000000000..0a512c17dcb25eb9699dd6472468ba9d220c1cad
--- /dev/null
+++ b/src/objective-c/tests/InteropTests.m
@@ -0,0 +1,305 @@
+/*
+ *
+ * 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.
+ *
+ */
+
+#include <grpc/status.h>
+
+#import <UIKit/UIKit.h>
+#import <XCTest/XCTest.h>
+
+#import <gRPC/GRXWriter+Immediate.h>
+#import <gRPC/GRXBufferedPipe.h>
+#import <gRPC/ProtoRPC.h>
+#import <RemoteTest/Empty.pbobjc.h>
+#import <RemoteTest/Messages.pbobjc.h>
+#import <RemoteTest/Test.pbobjc.h>
+#import <RemoteTest/Test.pbrpc.h>
+
+// Convenience constructors for the generated proto messages:
+
+@interface RMTStreamingOutputCallRequest (Constructors)
++ (instancetype)messageWithPayloadSize:(NSNumber *)payloadSize
+                 requestedResponseSize:(NSNumber *)responseSize;
+@end
+
+@implementation RMTStreamingOutputCallRequest (Constructors)
++ (instancetype)messageWithPayloadSize:(NSNumber *)payloadSize
+                 requestedResponseSize:(NSNumber *)responseSize {
+  RMTStreamingOutputCallRequest *request = [self message];
+  RMTResponseParameters *parameters = [RMTResponseParameters message];
+  parameters.size = responseSize.integerValue;
+  [request.responseParametersArray addObject:parameters];
+  request.payload.body = [NSMutableData dataWithLength:payloadSize.unsignedIntegerValue];
+  return request;
+}
+@end
+
+@interface RMTStreamingOutputCallResponse (Constructors)
++ (instancetype)messageWithPayloadSize:(NSNumber *)payloadSize;
+@end
+
+@implementation RMTStreamingOutputCallResponse (Constructors)
++ (instancetype)messageWithPayloadSize:(NSNumber *)payloadSize {
+  RMTStreamingOutputCallResponse * response = [self message];
+  response.payload.type = RMTPayloadType_Compressable;
+  response.payload.body = [NSMutableData dataWithLength:payloadSize.unsignedIntegerValue];
+  return response;
+}
+@end
+
+@interface InteropTests : XCTestCase
+@end
+
+@implementation InteropTests {
+  RMTTestService *_service;
+}
+
+- (void)setUp {
+  _service = [[RMTTestService alloc] initWithHost:@"grpc-test.sandbox.google.com"];
+}
+
+// Tests as described here: https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md
+
+- (void)testEmptyUnaryRPC {
+  __weak XCTestExpectation *expectation = [self expectationWithDescription:@"EmptyUnary"];
+
+  RMTEmpty *request = [RMTEmpty message];
+
+  [_service emptyCallWithRequest:request handler:^(RMTEmpty *response, NSError *error) {
+    XCTAssertNil(error, @"Finished with unexpected error: %@", error);
+
+    id expectedResponse = [RMTEmpty message];
+    XCTAssertEqualObjects(response, expectedResponse);
+
+    [expectation fulfill];
+  }];
+
+  [self waitForExpectationsWithTimeout:2 handler:nil];
+}
+
+- (void)testLargeUnaryRPC {
+  __weak XCTestExpectation *expectation = [self expectationWithDescription:@"LargeUnary"];
+
+  RMTSimpleRequest *request = [RMTSimpleRequest message];
+  request.responseType = RMTPayloadType_Compressable;
+  request.responseSize = 314159;
+  request.payload.body = [NSMutableData dataWithLength:271828];
+
+  [_service unaryCallWithRequest:request handler:^(RMTSimpleResponse *response, NSError *error) {
+    XCTAssertNil(error, @"Finished with unexpected error: %@", error);
+
+    RMTSimpleResponse *expectedResponse = [RMTSimpleResponse message];
+    expectedResponse.payload.type = RMTPayloadType_Compressable;
+    expectedResponse.payload.body = [NSMutableData dataWithLength:314159];
+    XCTAssertEqualObjects(response, expectedResponse);
+
+    [expectation fulfill];
+  }];
+
+  [self waitForExpectationsWithTimeout:4 handler:nil];
+}
+
+- (void)testClientStreamingRPC {
+  __weak XCTestExpectation *expectation = [self expectationWithDescription:@"ClientStreaming"];
+
+  RMTStreamingInputCallRequest *request1 = [RMTStreamingInputCallRequest message];
+  request1.payload.body = [NSMutableData dataWithLength:27182];
+
+  RMTStreamingInputCallRequest *request2 = [RMTStreamingInputCallRequest message];
+  request2.payload.body = [NSMutableData dataWithLength:8];
+
+  RMTStreamingInputCallRequest *request3 = [RMTStreamingInputCallRequest message];
+  request3.payload.body = [NSMutableData dataWithLength:1828];
+
+  RMTStreamingInputCallRequest *request4 = [RMTStreamingInputCallRequest message];
+  request4.payload.body = [NSMutableData dataWithLength:45904];
+
+  id<GRXWriter> writer = [GRXWriter writerWithContainer:@[request1, request2, request3, request4]];
+
+  [_service streamingInputCallWithRequestsWriter:writer
+                                         handler:^(RMTStreamingInputCallResponse *response,
+                                                   NSError *error) {
+    XCTAssertNil(error, @"Finished with unexpected error: %@", error);
+
+    RMTStreamingInputCallResponse *expectedResponse = [RMTStreamingInputCallResponse message];
+    expectedResponse.aggregatedPayloadSize = 74922;
+    XCTAssertEqualObjects(response, expectedResponse);
+
+    [expectation fulfill];
+  }];
+
+  [self waitForExpectationsWithTimeout:4 handler:nil];
+}
+
+- (void)testServerStreamingRPC {
+  __weak XCTestExpectation *expectation = [self expectationWithDescription:@"ServerStreaming"];
+
+  NSArray *expectedSizes = @[@31415, @9, @2653, @58979];
+
+  RMTStreamingOutputCallRequest *request = [RMTStreamingOutputCallRequest message];
+  for (NSNumber *size in expectedSizes) {
+    RMTResponseParameters *parameters = [RMTResponseParameters message];
+    parameters.size = [size integerValue];
+    [request.responseParametersArray addObject:parameters];
+  }
+
+  __block int index = 0;
+  [_service streamingOutputCallWithRequest:request
+                                   handler:^(BOOL done,
+                                             RMTStreamingOutputCallResponse *response,
+                                             NSError *error){
+    XCTAssertNil(error, @"Finished with unexpected error: %@", error);
+    XCTAssertTrue(done || response, @"Event handler called without an event.");
+
+    if (response) {
+      XCTAssertLessThan(index, 4, @"More than 4 responses received.");
+      id expected = [RMTStreamingOutputCallResponse messageWithPayloadSize:expectedSizes[index]];
+      XCTAssertEqualObjects(response, expected);
+      index += 1;
+    }
+
+    if (done) {
+      XCTAssertEqual(index, 4, @"Received %i responses instead of 4.", index);
+      [expectation fulfill];
+    }
+  }];
+
+  [self waitForExpectationsWithTimeout:4 handler:nil];
+}
+
+- (void)testPingPongRPC {
+  __weak XCTestExpectation *expectation = [self expectationWithDescription:@"PingPong"];
+
+  NSArray *requests = @[@27182, @8, @1828, @45904];
+  NSArray *responses = @[@31415, @9, @2653, @58979];
+
+  GRXBufferedPipe *requestsBuffer = [[GRXBufferedPipe alloc] init];
+
+  __block int index = 0;
+
+  id request = [RMTStreamingOutputCallRequest messageWithPayloadSize:requests[index]
+                                               requestedResponseSize:responses[index]];
+  [requestsBuffer writeValue:request];
+
+  [_service fullDuplexCallWithRequestsWriter:requestsBuffer
+                                     handler:^(BOOL done,
+                                               RMTStreamingOutputCallResponse *response,
+                                               NSError *error) {
+    XCTAssertNil(error, @"Finished with unexpected error: %@", error);
+    XCTAssertTrue(done || response, @"Event handler called without an event.");
+
+    if (response) {
+      XCTAssertLessThan(index, 4, @"More than 4 responses received.");
+      id expected = [RMTStreamingOutputCallResponse messageWithPayloadSize:responses[index]];
+      XCTAssertEqualObjects(response, expected);
+      index += 1;
+      if (index < 4) {
+        id request = [RMTStreamingOutputCallRequest messageWithPayloadSize:requests[index]
+                                                     requestedResponseSize:responses[index]];
+        [requestsBuffer writeValue:request];
+      } else {
+        [requestsBuffer writesFinishedWithError:nil];
+      }
+    }
+
+    if (done) {
+      XCTAssertEqual(index, 4, @"Received %i responses instead of 4.", index);
+      [expectation fulfill];
+    }
+  }];
+  [self waitForExpectationsWithTimeout:2 handler:nil];
+}
+
+- (void)testEmptyStreamRPC {
+  __weak XCTestExpectation *expectation = [self expectationWithDescription:@"EmptyStream"];
+  [_service fullDuplexCallWithRequestsWriter:[GRXWriter emptyWriter]
+                                     handler:^(BOOL done,
+                                               RMTStreamingOutputCallResponse *response,
+                                               NSError *error) {
+    XCTAssertNil(error, @"Finished with unexpected error: %@", error);
+    XCTAssert(done, @"Unexpected response: %@", response);
+    [expectation fulfill];
+  }];
+  [self waitForExpectationsWithTimeout:2 handler:nil];
+}
+
+- (void)testCancelAfterBeginRPC {
+  __weak XCTestExpectation *expectation = [self expectationWithDescription:@"CancelAfterBegin"];
+
+  // A buffered pipe to which we never write any value acts as a writer that just hangs.
+  GRXBufferedPipe *requestsBuffer = [[GRXBufferedPipe alloc] init];
+
+  ProtoRPC *call = [_service RPCToStreamingInputCallWithRequestsWriter:requestsBuffer
+                                                               handler:^(RMTStreamingInputCallResponse *response,
+                                                                         NSError *error) {
+    XCTAssertEqual(error.code, GRPC_STATUS_CANCELLED);
+    [expectation fulfill];
+  }];
+  [call start];
+  [call cancel];
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testCancelAfterFirstResponseRPC {
+  __weak XCTestExpectation *expectation = [self expectationWithDescription:@"CancelAfterFirstResponse"];
+  
+  // A buffered pipe to which we write a single value but never close
+  GRXBufferedPipe *requestsBuffer = [[GRXBufferedPipe alloc] init];
+  
+  __block BOOL receivedResponse = NO;
+  
+  id request = [RMTStreamingOutputCallRequest messageWithPayloadSize:@21782
+                                               requestedResponseSize:@31415];
+  
+  [requestsBuffer writeValue:request];
+  
+  __block ProtoRPC *call = [_service RPCToFullDuplexCallWithRequestsWriter:requestsBuffer
+                                                                   handler:^(BOOL done,
+                                                                             RMTStreamingOutputCallResponse *response,
+                                                                             NSError *error) {
+    if (receivedResponse) {
+      XCTAssert(done, @"Unexpected extra response %@", response);
+      XCTAssertEqual(error.code, GRPC_STATUS_CANCELLED);
+      [expectation fulfill];
+    } else {
+      XCTAssertNil(error, @"Finished with unexpected error: %@", error);
+      XCTAssertFalse(done, @"Finished without response");
+      XCTAssertNotNil(response);
+      receivedResponse = YES;
+      [call cancel];
+    }
+  }];
+  [call start];
+  [self waitForExpectationsWithTimeout:4 handler:nil];
+}
+
+@end
diff --git a/src/objective-c/tests/Tests.xcodeproj/project.pbxproj b/src/objective-c/tests/Tests.xcodeproj/project.pbxproj
index 80059b7cbe836a6317427b2f51315feb39dd258c..3a98fec11a321ac1b3189bd782c53a96784a042b 100644
--- a/src/objective-c/tests/Tests.xcodeproj/project.pbxproj
+++ b/src/objective-c/tests/Tests.xcodeproj/project.pbxproj
@@ -10,6 +10,7 @@
 		63423F4A1B150A5F006CF63C /* libTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 635697C71B14FC11007A7283 /* libTests.a */; };
 		63423F511B151B77006CF63C /* RxLibraryUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 63423F501B151B77006CF63C /* RxLibraryUnitTests.m */; };
 		635697CD1B14FC11007A7283 /* Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 635697CC1B14FC11007A7283 /* Tests.m */; };
+		635ED2EC1B1A3BC400FDE5C3 /* InteropTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 635ED2EB1B1A3BC400FDE5C3 /* InteropTests.m */; };
 		7D8A186224D39101F90230F6 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 35F2B6BF3BAE8F0DC4AFD76E /* libPods.a */; };
 /* End PBXBuildFile section */
 
@@ -43,6 +44,7 @@
 		635697C71B14FC11007A7283 /* libTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTests.a; sourceTree = BUILT_PRODUCTS_DIR; };
 		635697CC1B14FC11007A7283 /* Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Tests.m; sourceTree = "<group>"; };
 		635697D81B14FC11007A7283 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		635ED2EB1B1A3BC400FDE5C3 /* InteropTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InteropTests.m; sourceTree = "<group>"; };
 		FF7B5489BCFE40111D768DD0 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -105,6 +107,7 @@
 		635697C91B14FC11007A7283 /* Tests */ = {
 			isa = PBXGroup;
 			children = (
+				635ED2EB1B1A3BC400FDE5C3 /* InteropTests.m */,
 				63423F501B151B77006CF63C /* RxLibraryUnitTests.m */,
 				635697CC1B14FC11007A7283 /* Tests.m */,
 				635697D71B14FC11007A7283 /* Supporting Files */,
@@ -244,6 +247,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				63423F511B151B77006CF63C /* RxLibraryUnitTests.m in Sources */,
+				635ED2EC1B1A3BC400FDE5C3 /* InteropTests.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};