From ded5c19977f3c991c1ef72c6ee426a497623bc33 Mon Sep 17 00:00:00 2001 From: Tim Shadel Date: Thu, 11 Aug 2016 13:09:46 -0600 Subject: [PATCH 1/6] Create sessions directly with file URLs This so you can pass a cassette URL to your app in `launchEnvironment` that points to a resource in your UI testing target. --- DVR/Session.swift | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/DVR/Session.swift b/DVR/Session.swift index a09fa2a..b63764b 100644 --- a/DVR/Session.swift +++ b/DVR/Session.swift @@ -5,12 +5,10 @@ public class Session: NSURLSession { // MARK: - Properties public var outputDirectory: String - public let cassetteName: String + public var cassetteURL: NSURL? public let backingSession: NSURLSession public var recordingEnabled = true - private let testBundle: NSBundle - private var recording = false private var needsPersistence = false private var outstandingTasks = [NSURLSessionTask]() @@ -23,10 +21,14 @@ public class Session: NSURLSession { // MARK: - Initializers - public init(outputDirectory: String = "~/Desktop/DVR/", cassetteName: String, testBundle: NSBundle = NSBundle.allBundles().filter() { $0.bundlePath.hasSuffix(".xctest") }.first!, backingSession: NSURLSession = NSURLSession.sharedSession()) { + convenience public init(outputDirectory: String = "~/Desktop/DVR/", cassetteName: String, testBundle: NSBundle? = NSBundle.allBundles().filter() { $0.bundlePath.hasSuffix(".xctest") }.first, backingSession: NSURLSession = NSURLSession.sharedSession()) { + let bundle = testBundle ?? NSBundle.mainBundle() + self.init(outputDirectory: outputDirectory, cassetteURL: bundle.URLForResource(cassetteName, withExtension: "json"), backingSession: backingSession) + } + + public init(outputDirectory: String = "~/Desktop/DVR/", cassetteURL: NSURL?, backingSession: NSURLSession = NSURLSession.sharedSession()) { self.outputDirectory = outputDirectory - self.cassetteName = cassetteName - self.testBundle = testBundle + self.cassetteURL = cassetteURL self.backingSession = backingSession super.init() } @@ -110,8 +112,8 @@ public class Session: NSURLSession { // MARK: - Internal var cassette: Cassette? { - guard let path = testBundle.pathForResource(cassetteName, ofType: "json"), - data = NSData(contentsOfFile: path), + guard let cassetteURL = cassetteURL, + data = NSData(contentsOfURL: cassetteURL), raw = try? NSJSONSerialization.JSONObjectWithData(data, options: []), json = raw as? [String: AnyObject] else { return nil } @@ -195,6 +197,10 @@ public class Session: NSURLSession { } } + var cassetteName = "cassette" + if let s = cassetteURL?.lastPathComponent { + cassetteName = s.substringToIndex(s.endIndex.advancedBy(-5)) + } let cassette = Cassette(name: cassetteName, interactions: interactions) // Persist @@ -214,9 +220,9 @@ public class Session: NSURLSession { if let data = string.dataUsingEncoding(NSUTF8StringEncoding) { data.writeToFile(outputPath, atomically: true) print("[DVR] Persisted cassette at \(outputPath). Please add this file to your test target") + } else { + print("[DVR] Failed to persist cassette.") } - - print("[DVR] Failed to persist cassette.") } catch { print("[DVR] Failed to persist cassette.") } From 5357aea064c3e13ef4e0397ff4c47602e6de1668 Mon Sep 17 00:00:00 2001 From: Tim Shadel Date: Thu, 11 Aug 2016 13:29:49 -0600 Subject: [PATCH 2/6] Add info on setting up UI testing with DVR --- Readme.markdown | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Readme.markdown b/Readme.markdown index c343ef7..216c3b7 100644 --- a/Readme.markdown +++ b/Readme.markdown @@ -55,3 +55,32 @@ session.dataTaskWithRequest(NSURLRequest(URL: NSURL(string: "http://apple.com")! ``` If you don't call `beginRecording` and `endRecording`, DVR will call these for your around the first request you make to a session. You can call `endRecording` immediately after you've submitted all of your requests to the session. The optional completion block that `endRecording` accepts will be called when all requests have finished. This is a good spot to fulfill XCTest expectations you've setup or do whatever else now that networking has finished. + +### UI Testing + +To fake network requests in your app during UI testing, setup your app's network stack to check for the cassette file. + +``` swift +var activeSession: NSURLSession { + return fakeSession ?? defaultSession +} + +private var fakeSession: NSURLSession? { + guard let path = NSProcessInfo.processInfo().environment["cassette"] else { return nil } + return Session(cassetteURL: NSURL(string: path)!) +} +``` + +And then send your app the cassette file's location when you launch it during UI testing. + +``` swift +class LoginUITests: XCTestCase { + func testUseValidAuth() { + let app = XCUIApplication() + app.launchEnvironment["cassette"] = NSBundle(forClass: LoginUITests.self).URLForResource("valid-auth", withExtension: "json")!.absoluteString + app.launch() + } +} +``` + +If you use a URL that does not exist, DVR will record the result the same way it does in other testing scenarios. From b8c3cbe64735ced65ba7e7aaeae0093cca8742ed Mon Sep 17 00:00:00 2001 From: Tim Shadel Date: Thu, 11 Aug 2016 13:42:40 -0600 Subject: [PATCH 3/6] Fix up unit tests --- DVR/Tests/SessionTests.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/DVR/Tests/SessionTests.swift b/DVR/Tests/SessionTests.swift index 3ff4e05..5d52582 100644 --- a/DVR/Tests/SessionTests.swift +++ b/DVR/Tests/SessionTests.swift @@ -12,7 +12,15 @@ class SessionTests: XCTestCase { let request = NSURLRequest(URL: NSURL(string: "http://example.com")!) func testInit() { - XCTAssertEqual("example", session.cassetteName) + XCTAssertEqual("example.json", session.cassetteURL?.lastPathComponent) + XCTAssertTrue(NSFileManager.defaultManager().fileExistsAtPath(session.cassetteURL!.path!)) + } + + func testInitWithURL() { + let fileURL = NSBundle(forClass: SessionTests.self).URLForResource("example", withExtension: "json") + let alternateSession = Session(cassetteURL: fileURL) + XCTAssertEqual("example.json", alternateSession.cassetteURL?.lastPathComponent) + XCTAssertTrue(NSFileManager.defaultManager().fileExistsAtPath(alternateSession.cassetteURL!.path!)) } func testDataTask() { From 7cc9710b3b7de84b3db76c410ce2017f1a6552d4 Mon Sep 17 00:00:00 2001 From: Tim Shadel Date: Thu, 11 Aug 2016 14:05:22 -0600 Subject: [PATCH 4/6] Explicitly check failure points during testing This also catches and fixes the test failure on master involving a file that isn't being created because the directory didn't exist. --- DVR/Session.swift | 2 +- DVR/Tests/SessionUploadTests.swift | 25 ++++++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/DVR/Session.swift b/DVR/Session.swift index b63764b..5ec8029 100644 --- a/DVR/Session.swift +++ b/DVR/Session.swift @@ -66,7 +66,7 @@ public class Session: NSURLSession { } public override func uploadTaskWithRequest(request: NSURLRequest, fromFile fileURL: NSURL, completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionUploadTask { - let data = NSData(contentsOfURL: fileURL)! + let data = NSData(contentsOfURL: fileURL) return addUploadTask(request, fromData: data, completionHandler: completionHandler) } diff --git a/DVR/Tests/SessionUploadTests.swift b/DVR/Tests/SessionUploadTests.swift index f3009e8..155a2b9 100644 --- a/DVR/Tests/SessionUploadTests.swift +++ b/DVR/Tests/SessionUploadTests.swift @@ -12,8 +12,8 @@ class SessionUploadTests: XCTestCase { return request }() let multipartBoundary = "---------------------------3klfenalksjflkjoi9auf89eshajsnl3kjnwal".UTF8Data() - lazy var testFile: NSURL = { - return NSBundle(forClass: self.dynamicType).URLForResource("testfile", withExtension: "txt")! + lazy var testFile: NSURL? = { + return NSBundle(forClass: self.dynamicType).URLForResource("testfile", withExtension: "txt") }() func testUploadFile() { @@ -21,18 +21,26 @@ class SessionUploadTests: XCTestCase { session.recordingEnabled = false let expectation = expectationWithDescription("Network") - let data = encodeMultipartBody(NSData(contentsOfURL: testFile)!, parameters: [:]) + guard let testFile = testFile else { XCTFail("Missing test file URL"); return } + guard let fileData = NSData(contentsOfURL: testFile) else { XCTFail("Missing body data"); return } + let data = encodeMultipartBody(fileData, parameters: [:]) let file = writeDataToFile(data, fileName: "upload-file") session.uploadTaskWithRequest(request, fromFile: file) { data, response, error in + if let error = error { + XCTFail("Error uploading file: \(error)") + return + } + guard let data = data else { XCTFail("Missing request data"); return } + do { - let JSON = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as? [String: AnyObject] + let JSON = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] XCTAssertEqual("test file\n", (JSON?["form"] as? [String: AnyObject])?["file"] as? String) } catch { XCTFail("Failed to read JSON.") } - let HTTPResponse = response as! NSHTTPURLResponse + guard let HTTPResponse = response as? NSHTTPURLResponse else { XCTFail("Bad HTTP response"); return } XCTAssertEqual(200, HTTPResponse.statusCode) expectation.fulfill() @@ -46,6 +54,7 @@ class SessionUploadTests: XCTestCase { session.recordingEnabled = false let expectation = expectationWithDescription("Network") + guard let testFile = testFile else { XCTFail("Missing testfile URL"); return } let data = encodeMultipartBody(NSData(contentsOfURL: testFile)!, parameters: [:]) session.uploadTaskWithRequest(request, fromData: data) { data, response, error in @@ -87,6 +96,12 @@ class SessionUploadTests: XCTestCase { func writeDataToFile(data: NSData, fileName: String) -> NSURL { let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] let documentsURL = NSURL(fileURLWithPath: documentsPath, isDirectory: true) + + do { + try NSFileManager.defaultManager().createDirectoryAtURL(documentsURL, withIntermediateDirectories: true, attributes: nil) + } catch { + XCTFail("Failed to create documents directory \(documentsURL). Error \(error)") + } let url = documentsURL.URLByAppendingPathComponent(fileName + ".tmp") From a4523a887a1256365882a57422fb054b05e4389e Mon Sep 17 00:00:00 2001 From: Tim Shadel Date: Wed, 21 Sep 2016 16:36:51 -0600 Subject: [PATCH 5/6] Remove the path extension using URL methods instead of magic numbers --- DVR/Session.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/DVR/Session.swift b/DVR/Session.swift index 5ec8029..f3b14a8 100644 --- a/DVR/Session.swift +++ b/DVR/Session.swift @@ -197,10 +197,7 @@ public class Session: NSURLSession { } } - var cassetteName = "cassette" - if let s = cassetteURL?.lastPathComponent { - cassetteName = s.substringToIndex(s.endIndex.advancedBy(-5)) - } + let cassetteName = cassetteURL?.URLByDeletingPathExtension?.lastPathComponent ?? "cassette" let cassette = Cassette(name: cassetteName, interactions: interactions) // Persist From 00027502fd69c881022409050b36b5a119a308ba Mon Sep 17 00:00:00 2001 From: Tim Shadel Date: Wed, 21 Sep 2016 16:38:56 -0600 Subject: [PATCH 6/6] Simplify! --- DVR/Session.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/DVR/Session.swift b/DVR/Session.swift index f3b14a8..cb81184 100644 --- a/DVR/Session.swift +++ b/DVR/Session.swift @@ -217,8 +217,7 @@ public class Session: NSURLSession { if let data = string.dataUsingEncoding(NSUTF8StringEncoding) { data.writeToFile(outputPath, atomically: true) print("[DVR] Persisted cassette at \(outputPath). Please add this file to your test target") - } else { - print("[DVR] Failed to persist cassette.") + return } } catch { print("[DVR] Failed to persist cassette.")