Merge pull request #3180 from keymanapp/feat/ios/httpdownloader-unit-tests

feat(ios/engine): HTTPDownloader unit testing
This commit is contained in:
Joshua Horton 2020-05-29 12:57:35 +07:00 committed by GitHub
commit e8cbcd0adb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 420 additions and 3 deletions

View file

@ -126,6 +126,9 @@
C0D3F3601F9F3AD80055C7CF /* InstallableKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3F35F1F9F3AD80055C7CF /* InstallableKeyboard.swift */; };
C0E30C8C1FC40D0400C80416 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E30C8B1FC40D0400C80416 /* Storage.swift */; };
C0EF3E7B1F95B65300CE9BD4 /* KeymanWebDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0EF3E7A1F95B65300CE9BD4 /* KeymanWebDelegate.swift */; };
CE07E3D2247E3FB100DFA9D2 /* HTTPDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE07E3D1247E3FB100DFA9D2 /* HTTPDownloaderTests.swift */; };
CE07E3D6247E41DB00DFA9D2 /* URLSessionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE07E3D5247E41DB00DFA9D2 /* URLSessionMock.swift */; };
CE07E3D8247E43CB00DFA9D2 /* URLSessionDownloadTaskMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE07E3D7247E43CB00DFA9D2 /* URLSessionDownloadTaskMock.swift */; };
CE17ABDE23069E76005FBB14 /* LanguageResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE17ABDD23069E76005FBB14 /* LanguageResource.swift */; };
CE1E1EC12303C8CC001C7BE0 /* ResourceDownloadStatusToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1E1EC02303C8CC001C7BE0 /* ResourceDownloadStatusToolbar.swift */; };
CE1F67A32304EB3800FF6972 /* ResourceDownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F67A22304EB3800FF6972 /* ResourceDownloadManager.swift */; };
@ -174,6 +177,7 @@
CE808A4D236697D500713E6B /* Reachability.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A0FC9FC22D66D9E00D33F86 /* Reachability.framework */; };
CE808A4F236697D800713E6B /* XCGLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1687ACCC1FD8DE5300926D69 /* XCGLogger.framework */; };
CE808A532366980A00713E6B /* Zip.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 165C8B8B1FD8220600D4A78D /* Zip.framework */; };
CE88042F247F4B97009BCA1A /* ExpectationDownloaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE88042E247F4B97009BCA1A /* ExpectationDownloaderDelegate.swift */; };
CE8EDEB123F53D1A009E1FF6 /* FileManagementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A079DD1223194B100581263 /* FileManagementTests.swift */; };
CE8EDEB323F53F96009E1FF6 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */; };
CE976D4F23FA380700FFDF3A /* Reachability.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A0FC9FC22D66D9E00D33F86 /* Reachability.framework */; };
@ -420,6 +424,9 @@
C0ED71B21F6BB0B1002A2FD6 /* HTTPDownloadRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPDownloadRequest.swift; sourceTree = "<group>"; };
C0ED71B41F6BBFAF002A2FD6 /* HTTPDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPDownloader.swift; sourceTree = "<group>"; };
C0EF3E7A1F95B65300CE9BD4 /* KeymanWebDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeymanWebDelegate.swift; sourceTree = "<group>"; };
CE07E3D1247E3FB100DFA9D2 /* HTTPDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPDownloaderTests.swift; sourceTree = "<group>"; };
CE07E3D5247E41DB00DFA9D2 /* URLSessionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionMock.swift; sourceTree = "<group>"; };
CE07E3D7247E43CB00DFA9D2 /* URLSessionDownloadTaskMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDownloadTaskMock.swift; sourceTree = "<group>"; };
CE17ABDD23069E76005FBB14 /* LanguageResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageResource.swift; sourceTree = "<group>"; };
CE1E1EC02303C8CC001C7BE0 /* ResourceDownloadStatusToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceDownloadStatusToolbar.swift; sourceTree = "<group>"; };
CE1F67A22304EB3800FF6972 /* ResourceDownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceDownloadManager.swift; sourceTree = "<group>"; };
@ -443,6 +450,7 @@
CE7A26D723CEEC630005955C /* Colors+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Colors+Extension.swift"; sourceTree = "<group>"; };
CE7A26DA23CEEF640005955C /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
CE7ADD6523DE89FC00BC9A00 /* Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alerts.swift; sourceTree = "<group>"; };
CE88042E247F4B97009BCA1A /* ExpectationDownloaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpectationDownloaderDelegate.swift; sourceTree = "<group>"; };
CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = "<group>"; };
CE9B440B23FE49F000499CAB /* Migrations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Migrations.swift; sourceTree = "<group>"; };
CE9B440D23FE4E7400499CAB /* 12.0 Ad-hoc Migration.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "12.0 Ad-hoc Migration.bundle"; sourceTree = "<group>"; };
@ -684,6 +692,7 @@
CE9CD88123FCC4C7002BF2F8 /* TestUtils */,
CE37C38923FCC9AF007031C6 /* resources */,
9A079DD1223194B100581263 /* FileManagementTests.swift */,
CE07E3D1247E3FB100DFA9D2 /* HTTPDownloaderTests.swift */,
9A079DD3223194B100581263 /* Info.plist */,
CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */,
CE4459CD23FBBA8D003151FD /* AppDelegate.swift */,
@ -784,6 +793,16 @@
path = KeyboardRepository;
sourceTree = "<group>";
};
CE07E3D3247E418800DFA9D2 /* Downloading */ = {
isa = PBXGroup;
children = (
CE07E3D5247E41DB00DFA9D2 /* URLSessionMock.swift */,
CE07E3D7247E43CB00DFA9D2 /* URLSessionDownloadTaskMock.swift */,
CE88042E247F4B97009BCA1A /* ExpectationDownloaderDelegate.swift */,
);
path = Downloading;
sourceTree = "<group>";
};
CE25CCBB1F1DA72A005AA2BC /* HTTPRequest */ = {
isa = PBXGroup;
children = (
@ -829,6 +848,7 @@
CE9CD88123FCC4C7002BF2F8 /* TestUtils */ = {
isa = PBXGroup;
children = (
CE07E3D3247E418800DFA9D2 /* Downloading */,
CE9CD87F23FCC1CA002BF2F8 /* TestUtils.swift */,
CE9CD88223FCC4E3002BF2F8 /* UserDefaults.swift */,
CE37C38C23FCD41E007031C6 /* Keyboards.swift */,
@ -1043,6 +1063,7 @@
C06D37281F81F4E100F61AE0 /* Headers */,
C06D37291F81F4E100F61AE0 /* Resources */,
CEC0C66F2410B005003E1BCD /* ShellScript */,
CE880430247F6EB6009BCA1A /* ShellScript */,
);
buildRules = (
);
@ -1268,6 +1289,24 @@
shellPath = /bin/sh;
shellScript = "#if which swiftlint >/dev/null; then\n# swiftlint --config ../../.swiftlint.yml\n#else\n# echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\n#fi\n";
};
CE880430247F6EB6009BCA1A /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n. \"$KEYMAN_ROOT/resources/build/xcode-utils.sh\"\n\nphaseSetBundleVersions\n";
showEnvVarsInLog = 0;
};
CEC0C66F2410B005003E1BCD /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@ -1285,6 +1324,7 @@
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n. \"$KEYMAN_ROOT/resources/build/xcode-utils.sh\"\n\nif [ ${UPLOAD_SENTRY:-false} = true ]; then\n # Calls resource script to perform the dSYM upload\n phaseSentryDsymUpload \"keyman-ios\"\nfi\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
@ -1302,11 +1342,15 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CE07E3D2247E3FB100DFA9D2 /* HTTPDownloaderTests.swift in Sources */,
CE9B440C23FE49F000499CAB /* Migrations.swift in Sources */,
CE88042F247F4B97009BCA1A /* ExpectationDownloaderDelegate.swift in Sources */,
CE07E3D8247E43CB00DFA9D2 /* URLSessionDownloadTaskMock.swift in Sources */,
CE37C39123FCD617007031C6 /* LexicalModels.swift in Sources */,
CE37C38D23FCD41E007031C6 /* Keyboards.swift in Sources */,
CE9CD88323FCC4E3002BF2F8 /* UserDefaults.swift in Sources */,
CEF888D523FE786C00667693 /* KeyboardScaleTests.swift in Sources */,
CE07E3D6247E41DB00DFA9D2 /* URLSessionMock.swift in Sources */,
CE8EDEB323F53F96009E1FF6 /* VersionTests.swift in Sources */,
CEDFEF8F23FE43B700BECF39 /* MigrationTests.swift in Sources */,
CE9CD88023FCC1CA002BF2F8 /* TestUtils.swift in Sources */,

View file

@ -8,7 +8,7 @@
import Foundation
enum DownloadType {
enum DownloadType: String {
case downloadFile
}

View file

@ -24,10 +24,10 @@ class HTTPDownloader: NSObject {
var downloadSession: URLSession!
public var userInfo: [String: Any] = [:]
init(_ handler: HTTPDownloadDelegate?) {
init(_ handler: HTTPDownloadDelegate?, session: URLSession = .shared) {
super.init()
self.handler = handler
downloadSession = URLSession.shared
downloadSession = session
}
func addRequest(_ request: HTTPDownloadRequest) {

View file

@ -0,0 +1,143 @@
//
// HTTPDownloaderTests.swift
// KeymanEngineTests
//
// Created by Joshua Horton on 5/27/20.
// Copyright © 2020 SIL International. All rights reserved.
//
import XCTest
@testable import KeymanEngine
class HTTPDownloaderTests: XCTestCase {
var downloader: HTTPDownloader?
var mockedURLSession: TestUtils.Downloading.URLSessionMock?
override func setUp() {
mockedURLSession = TestUtils.Downloading.URLSessionMock()
downloader = HTTPDownloader(nil, session: mockedURLSession!)
}
override func tearDownWithError() throws {
let queueWasCleared = mockedURLSession!.queueIsEmpty
mockedURLSession = nil
downloader = nil
if !queueWasCleared {
throw NSError(domain: "Keyman",
code: 4,
userInfo: [NSLocalizedDescriptionKey: "A test did not fully utilize its queued mock results!"])
}
}
func testSingleRequestSuccess() throws {
// Simple, easy case: our local testing copy of the Khmer Angkor KMP file.
let mockedResult = TestUtils.Downloading.DownloadMockResult(location: TestUtils.Keyboards.khmerAngkorKMP, error: nil)
mockedURLSession?.queueMockResult(mockedResult)
let testDelegate = TestUtils.Downloading.ExpectationDownloaderDelegate()
downloader!.handler = testDelegate
let request = HTTPDownloadRequest(url: TestUtils.Keyboards.khmerAngkorKMP)
downloader!.addRequest(request)
testDelegate.expect(method: .RequestStarted, request: request)
testDelegate.expect(method: .RequestFinished, request: request)
testDelegate.expect(method: .QueueFinished, queue: downloader!)
XCTAssertEqual(downloader!.requestsCount, 1)
downloader!.run()
testDelegate.sequentialWait(testCase: self, timeout: 10)
}
func testSingleRequestFailure() throws {
// A twist on the previous version - the 'download' fails.
let mockedResult = TestUtils.Downloading.DownloadMockResult(location: nil, error: NSError(domain: "KeymanTests", code: 1, userInfo: nil))
mockedURLSession?.queueMockResult(mockedResult)
let testDelegate = TestUtils.Downloading.ExpectationDownloaderDelegate()
downloader!.handler = testDelegate
let request = HTTPDownloadRequest(url: TestUtils.Keyboards.khmerAngkorKMP)
downloader!.addRequest(request)
testDelegate.expect(method: .RequestStarted, request: request)
testDelegate.expect(method: .RequestFailed, request: request)
testDelegate.expect(method: .QueueFinished, queue: downloader!)
XCTAssertEqual(downloader!.requestsCount, 1)
downloader!.run()
testDelegate.sequentialWait(testCase: self, timeout: 10)
}
func testSequentialRequests() {
let mockedResult1 = TestUtils.Downloading.DownloadMockResult(location: TestUtils.Keyboards.khmerAngkorKMP, error: nil)
mockedURLSession?.queueMockResult(mockedResult1)
let mockedResult2 = TestUtils.Downloading.DownloadMockResult(location:
TestUtils.LexicalModels.mtntKMP, error: nil)
mockedURLSession?.queueMockResult(mockedResult2)
let testDelegate = TestUtils.Downloading.ExpectationDownloaderDelegate()
downloader!.handler = testDelegate
let request1 = HTTPDownloadRequest(url: TestUtils.Keyboards.khmerAngkorKMP)
downloader!.addRequest(request1)
testDelegate.expect(method: .RequestStarted, request: request1)
testDelegate.expect(method: .RequestFinished, request: request1)
let request2 = HTTPDownloadRequest(url: TestUtils.LexicalModels.mtntKMP)
downloader!.addRequest(request2)
testDelegate.expect(method: .RequestStarted, request: request2)
testDelegate.expect(method: .RequestFinished, request: request2)
testDelegate.expect(method: .QueueFinished, queue: downloader!)
XCTAssertEqual(downloader!.requestsCount, 2)
downloader!.run()
// This is currently safe, as all downloads are sequential. If we ever
// enable parallel downloads, this will obviously need to change
testDelegate.sequentialWait(testCase: self, timeout: 10)
}
func testRequestCancellation() {
let mockedResult1 = TestUtils.Downloading.DownloadMockResult(location: TestUtils.Keyboards.khmerAngkorKMP, error: nil)
mockedURLSession?.queueMockResult(mockedResult1)
// No second mocked result - its call should be cancelled.
let testDelegate = TestUtils.Downloading.ExpectationDownloaderDelegate()
downloader!.handler = testDelegate
let request1 = HTTPDownloadRequest(url: TestUtils.Keyboards.khmerAngkorKMP)
downloader!.addRequest(request1)
testDelegate.expect(method: .RequestStarted, request: request1)
let request2 = HTTPDownloadRequest(url: TestUtils.LexicalModels.mtntKMP)
downloader!.addRequest(request2)
testDelegate.expectAbsence(method: .RequestStarted, request: request2)
testDelegate.expectAbsence(method: .RequestFinished, request: request2)
testDelegate.expect(method: .QueueCancelled, queue: downloader!)
// Placed here for chronological ordering. We expect the request to finish because of
// our mocking structure, but want to delay its completion long enough for the
// `cancelAllOperations` call below to complete.
testDelegate.expect(method: .RequestFinished, request: request1, completion: { response in
// Force a delay so that our `cancelAllOperations` call has a chance to execute;
// our mocking structure is otherwise synchronous!
sleep(1) // in seconds.
response.expectation.fulfill()
return false
})
// Ideally, we could add this... but because of how things are currently mocked,
// it actually does trigger - we have a .RequestFinished processing after .QueueCancelled,
// which then notes the empty queue.
//
// testDelegate.expectAbsence(method: .QueueFinished, queue: downloader!)
XCTAssertEqual(downloader!.requestsCount, 2)
downloader!.run()
downloader!.cancelAllOperations()
// This is currently safe, as all downloads are sequential. If we ever
// enable parallel downloads, this will obviously need to change
testDelegate.sequentialWait(testCase: self, timeout: 5)
}
}

View file

@ -0,0 +1,138 @@
//
// BaseTestDownloaderDelegate.swift
// KeymanEngineTests
//
// Created by Joshua Horton on 5/28/20.
// Copyright © 2020 SIL International. All rights reserved.
//
import Foundation
import XCTest
@testable import KeymanEngine
extension TestUtils.Downloading {
class ExpectationDownloaderDelegate: HTTPDownloadDelegate {
enum Method: String {
case RequestStarted
case RequestFinished
case RequestFailed
case QueueFinished
case QueueCancelled
}
typealias ExpectationMetBlock = (ExpectationQueueEntry) -> Bool
struct ExpectationQueueEntry {
let method: Method
let request: HTTPDownloadRequest?
let queue: HTTPDownloader?
let completion: ExpectationMetBlock?
let expectation: XCTestExpectation
init(method: Method, request: HTTPDownloadRequest, completion: ExpectationMetBlock? = nil) {
self.init(method: method, request: request, queue: nil, completion: completion)
}
init(method: Method, queue: HTTPDownloader, completion: ExpectationMetBlock? = nil) {
self.init(method: method, request: nil, queue: queue, completion: completion)
}
private init(method: Method, request: HTTPDownloadRequest?, queue: HTTPDownloader?, completion: ExpectationMetBlock? = nil) {
self.method = method
self.request = request
self.queue = queue
self.completion = completion
// Now to dynamically build the description test for the Expectation, then the Expectation itself
var description: String
switch method {
case .RequestStarted, .RequestFinished, .RequestFailed:
description = "\(method.rawValue) for \(request!.typeCode.rawValue) from \(request!.url)"
case .QueueFinished, .QueueCancelled:
description = "\(method.rawValue)"
}
self.expectation = XCTestExpectation(description: description)
}
}
private var expectationQueue: [ExpectationQueueEntry] = []
private var expectedAbsences: [ExpectationQueueEntry] = []
private func doExpectationMatch(method: Method, request: HTTPDownloadRequest? = nil, queue: HTTPDownloader? = nil) {
if expectationQueue.count == 0 {
return
} else {
let head = expectationQueue[0]
if head.method == method && head.request == request && head.queue == queue {
expectationQueue.removeFirst()
// Now to look to fulfill the expectation.
if head.completion != nil {
if head.completion!(head) {
head.expectation.fulfill()
}
} else {
head.expectation.fulfill()
}
}
}
}
func downloadRequestStarted(_ request: HTTPDownloadRequest) {
doExpectationMatch(method: .RequestStarted, request: request)
}
func downloadRequestFinished(_ request: HTTPDownloadRequest) {
doExpectationMatch(method: .RequestFinished, request: request)
}
func downloadRequestFailed(_ request: HTTPDownloadRequest) {
doExpectationMatch(method: .RequestFailed, request: request)
}
func downloadQueueFinished(_ queue: HTTPDownloader) {
doExpectationMatch(method: .QueueFinished, queue: queue)
}
func downloadQueueCancelled(_ queue: HTTPDownloader) {
doExpectationMatch(method: .QueueCancelled, queue: queue)
}
func expect(method: Method, request: HTTPDownloadRequest, completion: ExpectationMetBlock? = nil) {
expectationQueue.append(ExpectationQueueEntry(method: method, request: request, completion: completion))
}
func expect(method: Method, queue: HTTPDownloader, completion: ExpectationMetBlock? = nil) {
expectationQueue.append(ExpectationQueueEntry(method: method, queue: queue, completion: completion))
}
func expectAbsence(method: Method, request: HTTPDownloadRequest, completion: ExpectationMetBlock? = nil) {
let entry = ExpectationQueueEntry(method: method, request: request, completion: completion)
entry.expectation.isInverted = true
expectedAbsences.append(entry)
}
func expectAbsence(method: Method, queue: HTTPDownloader, completion: ExpectationMetBlock? = nil) {
let entry = ExpectationQueueEntry(method: method, queue: queue, completion: completion)
entry.expectation.isInverted = true
expectedAbsences.append(entry)
}
func sequentialWait(testCase: XCTestCase, timeout: TimeInterval = 10) {
// while expectationQueue.count > 0 {
// testCase.wait(for: [expectationQueue[0].expectation], timeout: timeout)
// }
var expectationList: [XCTestExpectation] = expectationQueue.map { entry in
return entry.expectation
}
let absenceList: [XCTestExpectation] = expectedAbsences.map { entry in
return entry.expectation
}
expectationList.append(contentsOf: absenceList)
testCase.wait(for: expectationList, timeout: timeout, enforceOrder: true)
}
}
}

View file

@ -0,0 +1,30 @@
//
// URLSessionDownloadTaskMock.swift
// KeymanEngineTests
//
// Created by Joshua Horton on 5/27/20.
// Copyright © 2020 SIL International. All rights reserved.
//
import Foundation
extension TestUtils.Downloading {
/**
* Many thanks to https://www.swiftbysundell.com/articles/mocking-in-swift/ for the approach used here.
*/
class URLSessionDownloadTaskMock: URLSessionDownloadTask {
private let closure: () -> Void
init(closure: @escaping () -> Void) {
self.closure = closure
}
/*
* For mocked DownloadTasks, just use the precomputed completion closure
* provided to the constructor.
*/
override func resume() {
closure()
}
}
}

View file

@ -0,0 +1,57 @@
//
// URLSessionMock.swift
// KeymanEngineTests
//
// Created by Joshua Horton on 5/27/20.
// Copyright © 2020 SIL International. All rights reserved.
//
import Foundation
extension TestUtils.Downloading {
class DownloadMockResult {
let location: URL?
let error: Error?
init(location: URL?, error: Error?) {
self.location = location
self.error = error
}
}
/**
* Many thanks to https://www.swiftbysundell.com/articles/mocking-in-swift/ for the approach used here.
*/
class URLSessionMock: URLSession {
private var mockedResultQueue: [DownloadMockResult]
override init() {
mockedResultQueue = []
}
func queueMockResult(_ result: DownloadMockResult) {
mockedResultQueue.append(result)
}
var queueIsEmpty: Bool {
get {
return mockedResultQueue.isEmpty
}
}
override func downloadTask(with url: URL, completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask {
if(mockedResultQueue.count <= 0) {
fatalError("No mocked results have been provided for this downloadTask call!")
} else {
return URLSessionDownloadTaskMock {
let response = self.mockedResultQueue.removeFirst()
// A bit of white-box testing - we know that HTTPDownloader doesn't actually examine
// the URLResponse headers, so we don't provide any.
completionHandler(response.location, nil, response.error)
}
}
}
}
}

View file

@ -16,6 +16,11 @@ import Foundation
* It's a common Swift pattern, used because enums can't be instantiated.
*/
enum TestUtils {
// Sets up an internally-nested "namespace".
enum Downloading {
}
static let mainBundle = Bundle(for: FileManagementTests.self) // Can't use TestUtils, as it's an enum
static func findSubBundle(forResource resource: String, ofType type: String) -> Bundle {