Skip to content

Moving the StreamTest to integration tests #391

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 24, 2017
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Firestore/Example/Firestore.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */; };
AFE6114F0D4DAECBA7B7C089 /* Pods_Firestore_IntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */; };
C4E749275AD0FBDF9F4716A8 /* Pods_SwiftBuildTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */; };
D5B2532E4676014F57A7EAB9 /* FSTStreamTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D5B25C0D4AADFCA3ADB883E4 /* FSTStreamTests.m */; };
DE03B2C91F2149D600A30B9C /* FSTTransactionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */; };
DE03B2D41F2149D600A30B9C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; };
DE03B2D51F2149D600A30B9C /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; };
Expand Down Expand Up @@ -119,7 +120,6 @@
DE51B1F31F0D491B0013853F /* FSTDatastoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */; };
DE51B1F41F0D491B0013853F /* FSTRemoteEventTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */; };
DE51B1F61F0D491B0013853F /* FSTSerializerBetaTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */; };
DE51B1F71F0D491B0013853F /* FSTStreamTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B71F0D48AC0013853F /* FSTStreamTests.m */; };
DE51B1F81F0D491F0013853F /* FSTWatchChange+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */; };
DE51B1F91F0D491F0013853F /* FSTWatchChangeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */; };
DE51B1FA1F0D492C0013853F /* FSTLevelDBSpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */; };
Expand Down Expand Up @@ -222,6 +222,7 @@
B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.debug.xcconfig"; sourceTree = "<group>"; };
D3CC3DC5338DCAF43A211155 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
D5B25C0D4AADFCA3ADB883E4 /* FSTStreamTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSTStreamTests.m; sourceTree = "<group>"; };
DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests.release.xcconfig"; sourceTree = "<group>"; };
DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
DE03B3621F215E1600A30B9C /* CAcert.pem */ = {isa = PBXFileReference; lastKnownFileType = text; path = CAcert.pem; sourceTree = "<group>"; };
Expand Down Expand Up @@ -292,7 +293,6 @@
DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatastoreTests.m; sourceTree = "<group>"; };
DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteEventTests.m; sourceTree = "<group>"; };
DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSerializerBetaTests.m; sourceTree = "<group>"; };
DE51B1B71F0D48AC0013853F /* FSTStreamTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTStreamTests.m; sourceTree = "<group>"; };
DE51B1B81F0D48AC0013853F /* FSTWatchChange+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FSTWatchChange+Testing.h"; sourceTree = "<group>"; };
DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FSTWatchChange+Testing.m"; sourceTree = "<group>"; };
DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTWatchChangeTests.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -622,7 +622,6 @@
DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */,
DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */,
DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */,
DE51B1B71F0D48AC0013853F /* FSTStreamTests.m */,
DE51B1B81F0D48AC0013853F /* FSTWatchChange+Testing.h */,
DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */,
DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */,
Expand All @@ -639,6 +638,7 @@
DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */,
DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */,
DE51B1C71F0D48AC0013853F /* Util */,
D5B25C0D4AADFCA3ADB883E4 /* FSTStreamTests.m */,
);
path = Integration;
sourceTree = "<group>";
Expand Down Expand Up @@ -1162,7 +1162,6 @@
DE51B1E91F0D490D0013853F /* FSTLevelDBMutationQueueTests.mm in Sources */,
DE51B1E61F0D490D0013853F /* FSTRemoteDocumentCacheTests.m in Sources */,
DE51B1D91F0D490D0013853F /* FSTEagerGarbageCollectorTests.m in Sources */,
DE51B1F71F0D491B0013853F /* FSTStreamTests.m in Sources */,
DE51B1E21F0D490D0013853F /* FSTMutationQueueTests.m in Sources */,
DE51B1E81F0D490D0013853F /* FSTLevelDBKeyTests.mm in Sources */,
DE51B1E31F0D490D0013853F /* FSTPersistenceTestHelpers.m in Sources */,
Expand Down Expand Up @@ -1199,6 +1198,7 @@
DE03B2F21F214BAA00A30B9C /* FIRListenerRegistrationTests.m in Sources */,
DE03B2C91F2149D600A30B9C /* FSTTransactionTests.m in Sources */,
54DA12B11F315F3800DD57A1 /* FIRValidationTests.m in Sources */,
D5B2532E4676014F57A7EAB9 /* FSTStreamTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
263 changes: 263 additions & 0 deletions Firestore/Example/Tests/Integration/FSTStreamTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#import <XCTest/XCTest.h>

#import <Firestore/FIRFirestoreSettings.h>

#import "Auth/FSTEmptyCredentialsProvider.h"
#import "Core/FSTDatabaseInfo.h"
#import "FSTHelpers.h"
#import "FSTIntegrationTestCase.h"
#import "Model/FSTDatabaseID.h"
#import "Remote/FSTDatastore.h"
#import "Util/FSTAssert.h"
#import "Util/FSTDispatchQueue.h"

/** Exposes otherwise private methods for testing. */
@interface FSTStream (Testing)
- (void)writesFinishedWithError:(NSError *_Nullable)error;
@end

/**
* Implements FSTWatchStreamDelegate and FSTWriteStreamDelegate and supports waiting on callbacks
* via `fulfillOnCallback`.
*/
@interface FSTStreamStatusDelegate : NSObject <FSTWatchStreamDelegate, FSTWriteStreamDelegate>

- (instancetype)initFrom:(XCTestCase *)testCase
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming: first part of the label should end with "test case".

Also avoid connecting words in the labels for subsequent arguments unless they're critical to the meaning. It's appropriate in e.g. copyToFile:fromFile but not here, where you're just specifying the queue argument.

This should be: initWithTestCase:(XCTestCase *)testCase queue:(FSTDispatchQueue *)queue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was inspired by the name in FEventTester. I updated it to use the naming you suggested and removed the 'using' part.

usingQueue:(FSTDispatchQueue *)dispatchQueue NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;

@property(nonatomic, readonly) NSMutableArray<NSString *> *states;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider splitting out implementation properties into a separate interface with an empty category (as is our convention elsewhere). As it stands it's not clear which of these are implementation state and which are meant to be used from within tests that use this delegate.

Copy link
Contributor Author

@schmidt-sebastian schmidt-sebastian Oct 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only public-facing property was 'invokeCallbacks', but I am actually not using it in this PR (and have removed it from the other one as well).

I tried moving the other properties into their own category, but it looks like I then need to create my getters and setters by hand (even @synthesize doesn't work). There's some discussion about this here: https://stackoverflow.com/questions/14399991/how-can-i-declare-a-private-property-within-a-named-category

@property(atomic, readwrite) BOOL invokeCallbacks;
@property(nonatomic, weak) XCTestExpectation *expectation;
@property(nonatomic, weak, readonly) FSTStream *stream;
@property(nonatomic, weak, readonly) XCTestCase *testCase;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider making your declaration order match your assignment order in the constructor, so it's easy to verify everything is assigned.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@property(nonatomic, weak, readonly) FSTDispatchQueue *dispatchQueue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a lot of weak references in here which is a serious design smell. Are these actually necessary?

In general avoid weak references unless you specifically need them to break a reference cycle because they make your code very difficult to reason about.

In this case, you need the testCase to be weak because you're making this a member of that test case (which creates a cycle). The other things in here can be regular strong references.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's enough for 'testCase' to be weak. Everything else is only retained by the test case itself as well as 'FSTStreamStatusDelegate'.


@end

@implementation FSTStreamStatusDelegate

- (instancetype)initFrom:(XCTestCase *)testCase usingQueue:(FSTDispatchQueue *)dispatchQueue {
if (self = [super init]) {
_testCase = testCase;
_dispatchQueue = dispatchQueue;
_states = [NSMutableArray new];
}

return self;
}

- (void)watchStreamDidOpen {
[_states addObject:@"watchStreamDidOpen"];
[_expectation fulfill];
_expectation = nil;
}

- (void)writeStreamDidOpen {
[_states addObject:@"writeStreamDidOpen"];
[_expectation fulfill];
_expectation = nil;
}

- (void)writeStreamDidCompleteHandshake {
[_states addObject:@"writeStreamDidCompleteHandshake"];
[_expectation fulfill];
_expectation = nil;
}

- (void)writeStreamDidClose:(NSError *_Nullable)error {
[_states addObject:@"writeStreamDidClose"];
[_expectation fulfill];
_expectation = nil;
}

- (void)watchStreamDidClose:(NSError *_Nullable)error {
[_states addObject:@"watchStreamDidClose"];
[_expectation fulfill];
_expectation = nil;
}

- (void)watchStreamDidChange:(FSTWatchChange *)change
snapshotVersion:(FSTSnapshotVersion *)snapshotVersion {
[_states addObject:@"watchStreamDidChange"];
[_expectation fulfill];
_expectation = nil;
}

- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion
mutationResults:(NSArray<FSTMutationResult *> *)results {
[_states addObject:@"writeStreamDidReceiveResponseWithVersion"];
[_expectation fulfill];
_expectation = nil;
}

/**
* Executes 'block' using the provided FSTDispatchQueue and waits for any callback on this delegate
* to be called.
*/
- (void)awaitNotificationFromBlock:(void (^)(void))block {
FSTAssert(_expectation == nil, @"Previous expectation still active");
XCTestExpectation *expectation =
[self.testCase expectationWithDescription:@"awaitCallbackInBlock"];
_expectation = expectation;
[self.dispatchQueue dispatchAsync:block];
[self.testCase awaitExpectations];
}

@end

@interface FSTStreamTests : XCTestCase

@end

@implementation FSTStreamTests {
dispatch_queue_t _testQueue;
FSTDatabaseInfo *_databaseInfo;
FSTEmptyCredentialsProvider *_credentials;
FSTStreamStatusDelegate *_delegate;
FSTDispatchQueue *_workerDispatchQueue;

/** Single mutation to send to the write stream. */
NSArray<FSTMutation *> *_mutations;
}

- (void)setUp {
[super setUp];

_mutations = @[ FSTTestSetMutation(@"foo/bar", @{}) ];

FIRFirestoreSettings *settings = [FSTIntegrationTestCase settings];
FSTDatabaseID *databaseID =
[FSTDatabaseID databaseIDWithProject:[FSTIntegrationTestCase projectID]
database:kDefaultDatabaseID];

_databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID
persistenceKey:@"test-key"
host:settings.host
sslEnabled:settings.sslEnabled];
_testQueue = dispatch_queue_create("FSTStreamTestWorkerQueue", DISPATCH_QUEUE_SERIAL);
_workerDispatchQueue = [FSTDispatchQueue queueWith:_testQueue];
_credentials = [[FSTEmptyCredentialsProvider alloc] init];
}

- (FSTWriteStream *)setUpWriteStream {
FSTDatastore *datastore = [[FSTDatastore alloc] initWithDatabaseInfo:_databaseInfo
workerDispatchQueue:_workerDispatchQueue
credentials:_credentials];

_delegate = [[FSTStreamStatusDelegate alloc] initFrom:self usingQueue:_workerDispatchQueue];
return [datastore createWriteStreamWithDelegate:_delegate];
}

- (FSTWatchStream *)setUpWatchStream {
FSTDatastore *datastore = [[FSTDatastore alloc] initWithDatabaseInfo:_databaseInfo
workerDispatchQueue:_workerDispatchQueue
credentials:_credentials];

_delegate = [[FSTStreamStatusDelegate alloc] initFrom:self usingQueue:_workerDispatchQueue];
return [datastore createWatchStreamWithDelegate:_delegate];
}

/**
* Drains the test queue and asserts that all the observed callbacks (up to this point) match
* 'expectedStates'. Clears the list of observed callbacks on completion.
*/
- (void)verifyDelegateObservedStates:(NSArray<NSString *> *)expectedStates {
// Drain queue
dispatch_sync(_testQueue, ^{
});

XCTAssertEqualObjects(_delegate.states, expectedStates);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this also remove the states accumulated in the delegate so that subsequent assertions only need to assert about the new states?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, we can do this here, but it's not technically needed since delegate is scoped to the test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delegate is scoped to the test but not clearing this means that you can't reasonably make more than one assertion. I'm also fine just leaving this alone until we need to do that, but just wanted the intent of my comment to be clear.

[_delegate.states removeAllObjects];
}

/** Verifies that the watch stream does not issue an onClose callback after a call to stop(). */
- (void)testWatchStreamStopBeforeHandshake {
FSTWatchStream *watchStream = [self setUpWatchStream];

[_delegate awaitNotificationFromBlock:^{
[watchStream start];
}];

// Stop must not call watchStreamDidClose because the full implementation of the delegate could
// attempt to restart the stream in the event it had pending watches.
[_workerDispatchQueue dispatchAsync:^{
[watchStream stop];
}];

// Simulate a final callback from GRPC
[watchStream writesFinishedWithError:nil];

[self verifyDelegateObservedStates:@[ @"watchStreamDidOpen" ]];
}

/** Verifies that the write stream does not issue an onClose callback after a call to stop(). */
- (void)testWriteStreamStopBeforeHandshake {
FSTWriteStream *writeStream = [self setUpWriteStream];

[_delegate awaitNotificationFromBlock:^{
[writeStream start];
}];

// Don't start the handshake.

// Stop must not call watchStreamDidClose because the full implementation of the delegate could
// attempt to restart the stream in the event it had pending watches.
[_workerDispatchQueue dispatchAsync:^{
[writeStream stop];
}];

// Simulate a final callback from GRPC
[writeStream writesFinishedWithError:nil];

[self verifyDelegateObservedStates:@[ @"writeStreamDidOpen" ]];
}

- (void)testWriteStreamStopAfterHandshake {
FSTWriteStream *writeStream = [self setUpWriteStream];

[_delegate awaitNotificationFromBlock:^{
[writeStream start];
}];

// Writing before the handshake should throw
dispatch_sync(_testQueue, ^{
XCTAssertThrows([writeStream writeMutations:_mutations]);
});

[_delegate awaitNotificationFromBlock:^{
[writeStream writeHandshake];
}];

// Now writes should succeed
[_delegate awaitNotificationFromBlock:^{
[writeStream writeMutations:_mutations];
}];

[_workerDispatchQueue dispatchAsync:^{
[writeStream stop];
}];

[self verifyDelegateObservedStates:@[
@"writeStreamDidOpen", @"writeStreamDidCompleteHandshake",
@"writeStreamDidReceiveResponseWithVersion"
]];
}

@end
Loading