-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #11 from Lickability/mock-url-session
Mock URLSession Behavior for Testing
- Loading branch information
Showing
9 changed files
with
243 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
// | ||
// NetworkSession.swift | ||
// Networking | ||
// | ||
// Created by Michael Liberatore on 5/9/23. | ||
// Copyright © 2023 Lickability. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import Combine | ||
|
||
/// Describes an object that coordinates a group of related, network data transfer tasks. This protocol has a similar API to `URLSession` for the purpose of mocking. | ||
public protocol NetworkSession { | ||
|
||
/// Creates a task that retrieves the contents of a URL based on the specified URL request object, and calls a handler upon completion. | ||
/// - Parameters: | ||
/// - request: A URL request object that provides the URL, cache policy, request type, body data or body stream, and so on. | ||
/// - completionHandler: The completion handler to call when the load request is complete. This handler is executed on the delegate queue. | ||
/// - Returns: The new session data task. | ||
/// | ||
/// - Note: This documentation is pulled directly from `URLSession`. | ||
func makeDataTask(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> NetworkSessionDataTask | ||
|
||
/// Returns a publisher that wraps a URL session data task for a given URL request. | ||
/// | ||
/// The publisher publishes data when the task completes, or terminates if the task fails with an error. | ||
/// - Parameter request: The URL request for which to create a data task. | ||
/// - Returns: A publisher that wraps a data task for the URL request. | ||
/// | ||
/// - Note: This documentation is pulled directly from `URLSession`. | ||
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher | ||
} | ||
|
||
extension URLSession: NetworkSession { | ||
|
||
// MARK: - NetworkSession | ||
|
||
public func makeDataTask(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> NetworkSessionDataTask { | ||
return dataTask(with: request, completionHandler: completionHandler) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// | ||
// NetworkSessionDataTask.swift | ||
// NetworkingTests | ||
// | ||
// Created by Michael Liberatore on 5/9/23. | ||
// Copyright © 2023 Lickability. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
/// Describes a network session task that can be performed. This protocol has a similar API to `URLSessionDataTask` for the purpose of mocking. | ||
public protocol NetworkSessionDataTask { | ||
|
||
/// Resumes the task, if it is suspended. | ||
/// | ||
/// - Note: This documentation is pulled directly from `URLSessionTask`. | ||
func resume() | ||
} | ||
|
||
extension URLSessionDataTask: NetworkSessionDataTask { } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
// | ||
// MockNetworkSession.swift | ||
// NetworkingTests | ||
// | ||
// Created by Michael Liberatore on 5/9/23. | ||
// Copyright © 2023 Lickability. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import Networking | ||
|
||
/// A mocked version of `NetworkSession` to be used in tests. Allows specification of success or failure cases. | ||
class MockNetworkSession: NetworkSession { | ||
private let result: Result<Data, Error> | ||
|
||
/// Creates a new `MockNetworkSession`. | ||
/// - Parameter result: The expected result of network requests performed by this mock. | ||
init(result: Result<Data, Error>) { | ||
self.result = result | ||
} | ||
|
||
// MARK: - NetworkSession | ||
|
||
func makeDataTask(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> NetworkSessionDataTask { | ||
return MockNetworkSessionDataTask { [weak self] in | ||
switch self?.result { | ||
case .success(let data): | ||
let response = request.url.flatMap { HTTPURLResponse(url: $0, statusCode: 200, httpVersion: nil, headerFields: nil) } | ||
completionHandler(data, response, nil) | ||
case .failure(let error): | ||
let response = request.url.flatMap { HTTPURLResponse(url: $0, statusCode: 0, httpVersion: nil, headerFields: nil) } | ||
completionHandler(nil, response, error) | ||
case .none: | ||
assertionFailure("`MockNetworkSession` went out of scope. Keep a reference to it for the duration of your tests.") | ||
} | ||
} | ||
} | ||
|
||
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher { | ||
return URLSession.DataTaskPublisher(request: request, session: .shared) // not currently mocked. | ||
} | ||
} | ||
|
||
private class MockNetworkSessionDataTask: NetworkSessionDataTask { | ||
private let resumeClosure: () -> Void | ||
|
||
init(closure: @escaping () -> Void) { | ||
self.resumeClosure = closure | ||
} | ||
|
||
func resume() { | ||
resumeClosure() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
// | ||
// NetworkControllerTests.swift | ||
// NetworkingTests | ||
// | ||
// Created by Michael Liberatore on 5/9/23. | ||
// Copyright © 2023 Lickability. All rights reserved. | ||
// | ||
|
||
import XCTest | ||
@testable import Networking | ||
|
||
class NetworkControllerTests: XCTestCase { | ||
|
||
func testAsyncAwaitSendWithFailure() async throws { | ||
let networkController = NetworkController(networkSession: MockNetworkSession(result: .failure(NetworkError.noResponse))) | ||
|
||
do { | ||
let _: [Photo] = try await networkController.send(PhotoRequest.photosList, requestBehaviors: []) | ||
XCTFail("Should’ve caught an error before reaching here.") | ||
} | ||
catch { | ||
guard let networkError = error as? NetworkError else { | ||
XCTFail("Encountered an unexpected error type") | ||
return | ||
} | ||
|
||
switch networkError { | ||
case .decodingError, .noData, .noResponse, .unsuccessfulStatusCode: | ||
XCTFail("Encountered an unexpected NetworkError type") | ||
case .underlyingNetworkingError(let underlyingError): | ||
guard let underlyingNetworkError = underlyingError as? NetworkError else { | ||
XCTFail("Encountered an unexpected underlying error type") | ||
return | ||
} | ||
|
||
XCTAssertEqual(underlyingNetworkError.failureReason, NetworkError.noResponse.failureReason) | ||
} | ||
} | ||
} | ||
|
||
func testAsyncAwaitSendWithSuccess() async throws { | ||
let photos: [Photo] = [ | ||
Photo(albumId: 1, id: 1, title: "1", url: "", thumbnailUrl: ""), | ||
Photo(albumId: 1, id: 2, title: "1", url: "", thumbnailUrl: ""), | ||
Photo(albumId: 1, id: 3, title: "1", url: "", thumbnailUrl: "") | ||
] | ||
|
||
let encoder = JSONEncoder() | ||
do { | ||
let data = try encoder.encode(photos) | ||
let networkController = NetworkController(networkSession: MockNetworkSession(result: .success(data))) | ||
let photosResponse: NetworkResponse = try await networkController.send(PhotoRequest.photosList, requestBehaviors: []) | ||
let decodedData = photosResponse.data | ||
|
||
XCTAssertEqual(data, decodedData) | ||
} | ||
catch { | ||
XCTFail("Unexpected error occurred: \(error).") | ||
} | ||
} | ||
|
||
func testAsyncAwaitBehaviors() async throws { | ||
let networkController = NetworkController(networkSession: MockNetworkSession(result: .failure(NetworkError.noResponse))) | ||
|
||
var requestWillSendWasCalled = false | ||
var requestDidFinishWasCalled = false | ||
|
||
let behavior = TestBehavior { | ||
requestWillSendWasCalled = true | ||
XCTAssertFalse(requestDidFinishWasCalled, "We should’ve reached this point before `requestDidFinishWasCalled` became true.") | ||
} didFinishClosure: { | ||
requestDidFinishWasCalled = true | ||
} | ||
|
||
do { | ||
let _: [Photo] = try await networkController.send(PhotoRequest.photosList, requestBehaviors: [behavior]) | ||
XCTFail("Should’ve caught an error before reaching here.") | ||
} | ||
catch { | ||
XCTAssertTrue(requestWillSendWasCalled) | ||
XCTAssertTrue(requestDidFinishWasCalled) | ||
} | ||
} | ||
} | ||
|
||
private struct TestBehavior: RequestBehavior { | ||
let willSendClosure: () -> Void | ||
let didFinishClosure: () -> Void | ||
|
||
func requestWillSend(request: inout URLRequest) { | ||
willSendClosure() | ||
} | ||
|
||
func requestDidFinish(result: Result<NetworkResponse, NetworkError>) { | ||
didFinishClosure() | ||
} | ||
} |