TestsTested | ✓ |
LangLanguage | Obj-CObjective C |
License | MIT |
ReleasedLast Release | May 2016 |
Maintained by Jordan Zucker.
Easy XCTestCase subclass for recording and replaying network events for integration testing
This is an easy testing framework for recording and replaying network events for automated integration testing. In order to reduce tedium around creating network stubs, it records live network requests and responses and then replays them in subsequent runs (thanks to the fabulous OHHTTPStubs) so that your software can in continunous integration without flakiness.
To run the example project, clone the repo, and run pod install
from the Example directory first.
BeKindRewind is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod "BeKindRewind"
#import <BeKindRewind/BeKindRewind.h>
@interface BeKindRewindExampleTestCase : BKRTestCase
@end
@implementation BeKindRewindExampleTestCase
- (BOOL)isRecording {
return YES;
}
- (void)testSimpleNetworkCall {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"https://httpbin.org/get?test=test"]];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]];
// don't forget to create a test expectation, this has the __block annotation because to avoid a retain cycle
// XCTestExpectation is necessary for asynchronous network activity, BeKindRewind will take care of everything else
__block XCTestExpectation *networkExpectation = [self expectationWithDescription:@"network"];
NSURLSessionDataTask *basicGetTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
XCTAssertNil(error);
XCTAssertNotNil(response);
XCTAssertNotNil(data);
NSDictionary *dataDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
XCTAssertNil(error);
XCTAssertEqualObjects(dataDict[@"args"], @{@"test" : @"test"});
// fulfill the expectation
[networkExpectation fulfill];
}];
[basicGetTask resume];
// explicitly wait for the expectation
[self waitForExpectationsWithTimeout:5 handler:^(NSError *error) {
// Assert fail if timeout encounters an error
XCTAssertNil(error);
}];
}
@end
After running this test, files will be outputted to the console with a log similar to this:
2016-03-02 10:09:27.630 xctest[92201:14865359] <BKRRecordableVCR: 0x7fb0a9714ce0>: trying to write cassette to: /Users/jordanz/Library/Developer/CoreSimulator/Devices/611CC72A-11D4-4DD2-8471-FF2F65413BC7/data/Documents/BeKindRewindExampleTestCase.bundle/testSimpleNetworkCall.plist
Drag this into your project as a bundle named after your test case (it is named automatically).
Then flip the isRecording
value to NO:
- (BOOL)isRecording {
return NO;
}
Then on subsequent runs, the tests will use the recorded files to respond to matched network requests. It helps to ensure recordings are being used by asserting on information that is specific to your recordings. An example is modifying the above test so that it asserts on the Date header in the NSHTTPURLResponse object returned during the test execution. Here's an example of how to update the test after creating a recording.
#import <BeKindRewind/BeKindRewind.h>
@interface BeKindRewindExampleTestCase : BKRTestCase
@end
@implementation BeKindRewindExampleTestCase
- (BOOL)isRecording {
return NO;
}
- (void)testSimpleNetworkCall {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"https://httpbin.org/get?test=test"]];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]];
// don't forget to create a test expectation, this has the __block annotation to avoid a retain cycle
// XCTestExpectation is necessary for asynchronous network activity, BeKindRewind will take care of everything else
__block XCTestExpectation *networkExpectation = [self expectationWithDescription:@"network"];
NSURLSessionDataTask *basicGetTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
XCTAssertNil(error);
XCTAssertNotNil(response);
XCTAssertNotNil(data);
NSDictionary *dataDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
XCTAssertNil(error);
XCTAssertEqualObjects(dataDict[@"args"], @{@"test" : @"test"});
// Now this ensures that the recording returned is the stub and not an accidental "live" request. This is
// important for stable testing
XCTAssertEqualObjects([(NSHTTPURLResponse *)response allHeaderFields][@"Date"], @"Wed, 02 Mar 2016 18:09:28 GMT");
// fulfill the expectation
[networkExpectation fulfill];
}];
[basicGetTask resume];
// explicitly wait for the expectation
[self waitForExpectationsWithTimeout:5 handler:^(NSError *error) {
// Assert fail if timeout encounters an error
XCTAssertNil(error);
}];
}
@end
These are set automatically. Feel free to override with appropriate values but it is not necessary if these will suffice. It is possible these defaults will change until version 1.0 lands. A note about recording and playback, they are only valid after [super setUp]
and before [super tearDown]
- (BOOL)isRecording {
return YES;
}
- (NSString *)baseFixturesDirectoryFilePath {
return [BKRTestCaseFilePathHelper documentsDirectory];
}
- (BKRTestConfiguration *)testConfiguration {
return [BKRTestConfiguration defaultConfigurationWithTestCase:self];
}
- (id<BKRTestVCRActions>)testVCRWithConfiguration:(BKRTestConfiguration *)configuration {
return [BKRTestVCR vcrWithTestConfiguration:configuration];
}
- (NSString *)recordingCassetteFilePathWithBaseDirectoryFilePath:(NSString *)baseDirectoryFilePath {
NSParameterAssert(baseDirectoryFilePath);
return [BKRTestCaseFilePathHelper writingFinalPathForTestCase:self inTestSuiteBundleInDirectory:baseDirectoryFilePath];
}
- (BKRCassette *)playingCassette {
NSDictionary *cassetteDictionary = [BKRTestCaseFilePathHelper dictionaryForTestCase:self];
XCTAssertNotNil(cassetteDictionary);
return [BKRCassette cassetteFromDictionary:cassetteDictionary];
}
- (BKRCassette *)recordingCassette {
return [BKRCassette cassette];
}
You may find during the course of your testing that you want information when a request is not matched. In this case, you can add a block to the BKRTestConfiguration
object that will be executed when a request fails to be matched during playing. This is useful for debugging and can be even be used to fail a test if you require strictness in matching.
Here is an example of how to configure your subclass of BKRTestCase
to log failed requests:
- (BKRTestConfiguration *)testConfiguration {
BKRTestConfiguration *configuration = [super testConfiguration];
configuration.requestMatchingFailedBlock = ^void (NSURLRequest *request) {
NSLog(@"Failed to match request: %@", request);
};
return configuration;
}
Here is an example of how to make a stricter XCTestCase
by failing the test case if a match fails to be found:
- (BKRTestConfiguration *)testConfiguration {
BKRTestConfiguration *configuration = [super testConfiguration];
configuration.requestMatchingFailedBlock = ^void (NSURLRequest *request) {
XCTFail(@"Failed to match request: %@", request);
};
return configuration;
}
BeKindRewind will only record network events if the NSURLSessionTask is sent a resume
message. When NSURLSessionTask objects are created, they are in a NSURLSessionTaskStateSuspended and will not start recording until the resume
is sent. Once the resume
is sent, it will record everything until the end of the test (protected by your XCTestExpectation
It is recommended you use the BKRTestVCR subclass for recording. It automatically handles issues around asynchronous execution and XCTestCase.
By default, BeKindRewind expects a Property List fixture to exist for every test case when it is playing back (mocking) network activity. If no fixture exists, then an exception is thrown. This can be overridden in the BKRTestConfiguration (or it's super class BKRConfiguration) object.
If you see an exception during testing similar to *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'API violation - creating expectations while already in waiting mode.'
then you probably need to adjust your BKRTestConfiguration
object. The default constructor for BKRTestConfiguration
(defaultConfigurationWithTestCase:
) calls a block when a network action begins and ends to create an XCTestExpectation
around a network action so that the recording is not clipped. If you have a series of network actions that are triggered during a test, some of which occur after the waitForExpectationsWithTimeout: handler:
is called, then you should set these blocks to nil for the duration of that test case. This should be overridden in your BKRTestCase
subclass (or wherever is appropriate if you are implementing a BKRVCR
yourself. An example of overriding this in BKRTestCase
is below:
- (BKRTestConfiguration *)testConfiguration {
BKRTestConfiguration *defaultConfiguration = [super testConfiguration];
defaultConfiguration.beginRecordingBlock = nil;
defaultConfiguration.endRecordingBlock = nil;
return defaultConfiguration;
}
If you see test failures due to an asynchronous wait timeout for an XCTestExpectation named reset
then that is most likely due to the test case trying to stub requests while removing stubs. The failure will look something like this:
[21:32:10]: ▸ ✗ testCopyConfigurationWithSubscribedChannelsAndCallbackQueue, ((error) == nil) failed: "Error Domain=com.apple.XCTestErrorDomain Code=0 "The operation couldn’t be completed. (com.apple.XCTestErrorDomain error 0.)""
[21:32:10]: ▸ ✗ testCopyConfigurationWithSubscribedChannelsAndCallbackQueue, Asynchronous wait failed: Exceeded timeout of 60 seconds, with unfulfilled expectations: "reset".
In order to fix this, make sure to properly tear down and stop any long running or background processes that are asynchronous, especially those related to networking, so that this race condition isn't introduced into your testing.
Try to avoid writing a test that is dependent upon state. Instead, ensure that when isRecording == YES
that the test can be fully recorded for playback, including setUp and tearDown. This eases development and ensures that the test isn't written on a condition that wouldn't be recreated when another developer tries to update your test with a new recording.
Use version 0.6.x or lower of JSZVCR if you want to test against iOS 7 or lower.
Jordan Zucker, [email protected]
BeKindRewind is available under the MIT license. See the LICENSE file for more info.