Skip to content
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

Support UI Testing #55

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 17 additions & 11 deletions DVR/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]()
Expand All @@ -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()
}
Expand Down Expand Up @@ -64,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)
}

Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -195,6 +197,10 @@ public class Session: NSURLSession {
}
}

var cassetteName = "cassette"
if let s = cassetteURL?.lastPathComponent {
cassetteName = s.substringToIndex(s.endIndex.advancedBy(-5))
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be much better to get the length directly instead of guessing it's 5

}
let cassette = Cassette(name: cassetteName, interactions: interactions)

// Persist
Expand All @@ -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.")
Copy link
Contributor

Choose a reason for hiding this comment

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

In my PR to convert to Swift, I added a return on 223 instead. Slightly more concise :)

}

print("[DVR] Failed to persist cassette.")
} catch {
print("[DVR] Failed to persist cassette.")
}
Expand Down
10 changes: 9 additions & 1 deletion DVR/Tests/SessionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
25 changes: 20 additions & 5 deletions DVR/Tests/SessionUploadTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,35 @@ 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")
Copy link
Contributor

Choose a reason for hiding this comment

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

👎 In tests, it's better to ! things so you can see the error right away durning the test.

}()

func testUploadFile() {
let session = Session(cassetteName: "upload-file")
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 }
Copy link
Contributor

Choose a reason for hiding this comment

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

👎 No sense in doing this. The test will fail if it's nil anyway.


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()
Expand All @@ -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
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here: try!

Copy link
Author

Choose a reason for hiding this comment

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

Actually all the !s were what was keeping you guys from having passing tests on Travis. I had to go through and remove them all to actually see what was truly failing and this piece here was the crux of the fix. The mac tests would pass and the iOS tests would fail because this file was expected but didn't exist. Travis (xcodebuild) only reports:

      -[SessionUploadTests testUploadFile]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
fatal error: unexpectedly found nil while unwrapping an Optional value
Child process terminated with signal 4: Illegal instruction
Test crashed while running.

Since there's no indication of where this crashed, and the test passed in Xcode itself, the actual error took like ½ a day to track down and was super annoying. So, while I like the theory that !s are awesome in testing, this bug totally proves them to still be harmful.

Copy link
Author

Choose a reason for hiding this comment

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

@soffes Do you want me to put all the !s back anyway?

} catch {
XCTFail("Failed to create documents directory \(documentsURL). Error \(error)")
}

let url = documentsURL.URLByAppendingPathComponent(fileName + ".tmp")

Expand Down
29 changes: 29 additions & 0 deletions Readme.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

✨ so cool!

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.