Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications •...

174
#WWDC18 © 2018 Apple Inc. All rights reserved. Redistribution or public display not permitted without written permission from Apple. Brian Croom, XCTest Engineer Stuart Montgomery, XCTest Engineer Testing Tips & Tricks Session 417

Transcript of Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications •...

Page 1: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

#WWDC18

© 2018 Apple Inc. All rights reserved. Redistribution or public display not permitted without written permission from Apple.

Brian Croom, XCTest Engineer Stuart Montgomery, XCTest Engineer

•Testing Tips & Tricks • Session 417

Page 2: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests
Page 3: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests
Page 4: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

•Testing network requests •Working with notifications •Mocking with protocols •Test execution speed

Page 5: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

•Testing network requests •Working with notifications •Mocking with protocols •Test execution speed

Page 6: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Pyramid of Tests Recap

•End-to-end

•Unit

•Integration

Engineering for Testability WWDC 2017

Page 7: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Pyramid of Tests

•Unit

Page 8: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Pyramid of Tests

•Integration

Page 9: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Pyramid of Tests•End-to-end

Page 10: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Pyramid of Tests•End-to-end

•Unit

•Integration

Page 11: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Networking Stack

App Update ViewCreate URLSession TaskPrepare URLRequest Parse Response

Server

App

Page 12: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

func loadData(near coord: CLLocationCoordinate2D) { let url = URL(string: "/locations?lat=\(coord.latitude)&long=\(coord.longitude)")! URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data else { self.handleError(error); return } do { let values = try JSONDecoder().decode([PointOfInterest].self, from: data)

DispatchQueue.main.async { self.tableValues = values self.tableView.reloadData() } } catch { self.handleError(error) } }.resume()}

Page 13: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

func loadData(near coord: CLLocationCoordinate2D) { let url = URL(string: "/locations?lat=\(coord.latitude)&long=\(coord.longitude)")! URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data else { self.handleError(error); return } do { let values = try JSONDecoder().decode([PointOfInterest].self, from: data)

DispatchQueue.main.async { self.tableValues = values self.tableView.reloadData() } } catch { self.handleError(error) } }.resume()}

Page 14: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

func loadData(near coord: CLLocationCoordinate2D) { let url = URL(string: "/locations?lat=\(coord.latitude)&long=\(coord.longitude)")! URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data else { self.handleError(error); return } do { let values = try JSONDecoder().decode([PointOfInterest].self, from: data)

DispatchQueue.main.async { self.tableValues = values self.tableView.reloadData() } } catch { self.handleError(error) } }.resume()}

Page 15: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

func loadData(near coord: CLLocationCoordinate2D) { let url = URL(string: "/locations?lat=\(coord.latitude)&long=\(coord.longitude)")! URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data else { self.handleError(error); return } do { let values = try JSONDecoder().decode([PointOfInterest].self, from: data)

DispatchQueue.main.async { self.tableValues = values self.tableView.reloadData() } } catch { self.handleError(error) } }.resume()}

Page 16: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

func loadData(near coord: CLLocationCoordinate2D) { let url = URL(string: "/locations?lat=\(coord.latitude)&long=\(coord.longitude)")! URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data else { self.handleError(error); return } do { let values = try JSONDecoder().decode([PointOfInterest].self, from: data)

DispatchQueue.main.async { self.tableValues = values self.tableView.reloadData() } } catch { self.handleError(error) } }.resume()}

Page 17: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

func loadData(near coord: CLLocationCoordinate2D) { let url = URL(string: "/locations?lat=\(coord.latitude)&long=\(coord.longitude)")! URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data else { self.handleError(error); return } do { let values = try JSONDecoder().decode([PointOfInterest].self, from: data)

DispatchQueue.main.async { self.tableValues = values self.tableView.reloadData() } } catch { self.handleError(error) } }.resume()}

Page 18: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

func loadData(near coord: CLLocationCoordinate2D) { let url = URL(string: "/locations?lat=\(coord.latitude)&long=\(coord.longitude)")! URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data else { self.handleError(error); return } do { let values = try JSONDecoder().decode([PointOfInterest].self, from: data)

DispatchQueue.main.async { self.tableValues = values self.tableView.reloadData() } } catch { self.handleError(error) } }.resume()}

Page 19: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

func loadData(near coord: CLLocationCoordinate2D) { let url = URL(string: "/locations?lat=\(coord.latitude)&long=\(coord.longitude)")! URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data else { self.handleError(error); return } do { let values = try JSONDecoder().decode([PointOfInterest].self, from: data)

DispatchQueue.main.async { self.tableValues = values self.tableView.reloadData() } } catch { self.handleError(error) } }.resume()}

Page 20: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Unit Tests

App Update ViewCreate URLSession Task

Server

Parse ResponsePrepare URLRequest Create URLSession Task

App

Update ViewCreate URLSession Task

Page 21: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Unit Tests

App Update ViewCreate URLSession Task

Server

Parse ResponsePrepare URLRequest Create URLSession Task

App

Parse ResponsePrepare URLRequest Update ViewCreate URLSession Task

Page 22: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Unit Tests

App Update ViewCreate URLSession Task

Server

Parse ResponsePrepare URLRequest Create URLSession Task

App

Parse ResponsePrepare URLRequest

Page 23: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

struct PointsOfInterestRequest {

func makeRequest(from coordinate: CLLocationCoordinate2D) throws -> URLRequest {

guard CLLocationCoordinate2DIsValid(coordinate) else {

throw RequestError.invalidCoordinate

}

var components = URLComponents(string: "https://example.com/locations")!

components.queryItems = [ URLQueryItem(name: "lat", value: "\(coordinate.latitude)"),

URLQueryItem(name: "long", value: "\(coordinate.longitude)") ]

return URLRequest(url: components.url!)

} func parseResponse(data: Data) throws -> [PointOfInterest] {

return try JSONDecoder().decode([PointOfInterest].self, from: data)

}

}

Page 24: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

struct PointsOfInterestRequest {

func makeRequest(from coordinate: CLLocationCoordinate2D) throws -> URLRequest {

guard CLLocationCoordinate2DIsValid(coordinate) else {

throw RequestError.invalidCoordinate

}

var components = URLComponents(string: "https://example.com/locations")!

components.queryItems = [ URLQueryItem(name: "lat", value: "\(coordinate.latitude)"),

URLQueryItem(name: "long", value: "\(coordinate.longitude)") ]

return URLRequest(url: components.url!)

} func parseResponse(data: Data) throws -> [PointOfInterest] {

return try JSONDecoder().decode([PointOfInterest].self, from: data)

}

}

Page 25: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class PointOfInterestRequestTests: XCTestCase { let request = PointsOfInterestRequest()

func testMakingURLRequest() throws { let coordinate = CLLocationCoordinate2D(latitude: 37.3293, longitude: -121.8893)

let urlRequest = try request.makeRequest(from: coordinate)

XCTAssertEqual(urlRequest.url?.scheme, "https") XCTAssertEqual(urlRequest.url?.host, "example.com") XCTAssertEqual(urlRequest.url?.query, "lat=37.3293&long=-121.8893") } }

Page 26: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class PointOfInterestRequestTests: XCTestCase { let request = PointsOfInterestRequest()

func testMakingURLRequest() throws { let coordinate = CLLocationCoordinate2D(latitude: 37.3293, longitude: -121.8893)

let urlRequest = try request.makeRequest(from: coordinate)

XCTAssertEqual(urlRequest.url?.scheme, "https") XCTAssertEqual(urlRequest.url?.host, "example.com") XCTAssertEqual(urlRequest.url?.query, "lat=37.3293&long=-121.8893") } }

Page 27: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class PointOfInterestRequestTests: XCTestCase { let request = PointsOfInterestRequest()

func testParsingResponse() throws { let jsonData = "[{\"name\":\"My Location\"}]".data(using: .utf8)! let response = try request.parseResponse(data: jsonData) XCTAssertEqual(response, [PointOfInterest(name: "My Location")]) } }

Page 28: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class PointOfInterestRequestTests: XCTestCase { let request = PointsOfInterestRequest()

func testParsingResponse() throws { let jsonData = "[{\"name\":\"My Location\"}]".data(using: .utf8)! let response = try request.parseResponse(data: jsonData) XCTAssertEqual(response, [PointOfInterest(name: "My Location")]) } }

Page 29: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Unit Tests

App Update ViewCreate URLSession Task

Server

Parse ResponsePrepare URLRequest

App

Parse ResponsePrepare URLRequest

Page 30: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Unit Tests

App Update ViewCreate URLSession Task

Server

Parse ResponsePrepare URLRequest

App

Create URLSession Task

Page 31: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

protocol APIRequest { associatedtype RequestDataType associatedtype ResponseDataType

func makeRequest(from data: RequestDataType) throws -> URLRequest func parseResponse(data: Data) throws -> ResponseDataType}

class APIRequestLoader<T: APIRequest> { let apiRequest: T let urlSession: URLSession

init(apiRequest: T, urlSession: URLSession = .shared) { self.apiRequest = apiRequest self.urlSession = urlSession }}

Page 32: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

protocol APIRequest { associatedtype RequestDataType associatedtype ResponseDataType

func makeRequest(from data: RequestDataType) throws -> URLRequest func parseResponse(data: Data) throws -> ResponseDataType}

class APIRequestLoader<T: APIRequest> { let apiRequest: T let urlSession: URLSession

init(apiRequest: T, urlSession: URLSession = .shared) { self.apiRequest = apiRequest self.urlSession = urlSession }}

Page 33: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

protocol APIRequest { associatedtype RequestDataType associatedtype ResponseDataType

func makeRequest(from data: RequestDataType) throws -> URLRequest func parseResponse(data: Data) throws -> ResponseDataType}

class APIRequestLoader<T: APIRequest> { let apiRequest: T let urlSession: URLSession

init(apiRequest: T, urlSession: URLSession = .shared) { self.apiRequest = apiRequest self.urlSession = urlSession }}

Page 34: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

protocol APIRequest { associatedtype RequestDataType associatedtype ResponseDataType

func makeRequest(from data: RequestDataType) throws -> URLRequest func parseResponse(data: Data) throws -> ResponseDataType}

class APIRequestLoader<T: APIRequest> { let apiRequest: T let urlSession: URLSession

init(apiRequest: T, urlSession: URLSession = .shared) { self.apiRequest = apiRequest self.urlSession = urlSession }}

Page 35: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

protocol APIRequest { associatedtype RequestDataType associatedtype ResponseDataType

func makeRequest(from data: RequestDataType) throws -> URLRequest func parseResponse(data: Data) throws -> ResponseDataType}

class APIRequestLoader<T: APIRequest> { let apiRequest: T let urlSession: URLSession

init(apiRequest: T, urlSession: URLSession = .shared) { self.apiRequest = apiRequest self.urlSession = urlSession }}

Page 36: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class APIRequestLoader<T: APIRequest> { func loadAPIRequest(requestData: T.RequestDataType, completionHandler: @escaping (T.ResponseDataType?, Error?) -> Void) { do { let urlRequest = try apiRequest.makeRequest(from: requestData) urlSession.dataTask(with: urlRequest) { data, response, error in guard let data = data else { return completionHandler(nil, error) } do { let parsedResponse = try self.apiRequest.parseResponse(data: data) completionHandler(parsedResponse, nil) } catch { completionHandler(nil, error) } }.resume() } catch { return completionHandler(nil, error) } }}

Page 37: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class APIRequestLoader<T: APIRequest> { func loadAPIRequest(requestData: T.RequestDataType, completionHandler: @escaping (T.ResponseDataType?, Error?) -> Void) { do { let urlRequest = try apiRequest.makeRequest(from: requestData) urlSession.dataTask(with: urlRequest) { data, response, error in guard let data = data else { return completionHandler(nil, error) } do { let parsedResponse = try self.apiRequest.parseResponse(data: data) completionHandler(parsedResponse, nil) } catch { completionHandler(nil, error) } }.resume() } catch { return completionHandler(nil, error) } }}

Page 38: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class APIRequestLoader<T: APIRequest> { func loadAPIRequest(requestData: T.RequestDataType, completionHandler: @escaping (T.ResponseDataType?, Error?) -> Void) { do { let urlRequest = try apiRequest.makeRequest(from: requestData) urlSession.dataTask(with: urlRequest) { data, response, error in guard let data = data else { return completionHandler(nil, error) } do { let parsedResponse = try self.apiRequest.parseResponse(data: data) completionHandler(parsedResponse, nil) } catch { completionHandler(nil, error) } }.resume() } catch { return completionHandler(nil, error) } }}

Page 39: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class APIRequestLoader<T: APIRequest> { func loadAPIRequest(requestData: T.RequestDataType, completionHandler: @escaping (T.ResponseDataType?, Error?) -> Void) { do { let urlRequest = try apiRequest.makeRequest(from: requestData) urlSession.dataTask(with: urlRequest) { data, response, error in guard let data = data else { return completionHandler(nil, error) } do { let parsedResponse = try self.apiRequest.parseResponse(data: data) completionHandler(parsedResponse, nil) } catch { completionHandler(nil, error) } }.resume() } catch { return completionHandler(nil, error) } }}

Page 40: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Unit Tests

App Update ViewPrepare URLRequest Parse Response

Server

App

Create URLSession Task

Page 41: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Integration Tests

App Update View

Server

App

Create URLSession TaskPrepare URLRequest Parse Response

Page 42: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

How to Use URLProtocol

URLSession URLSessionConfiguration

Page 43: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

How to Use URLProtocol

URLSession

URLSessionDataTask

URLSessionConfiguration

Page 44: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

How to Use URLProtocol

URLProtocol subclassesURLProtocol subclassesURLProtocol subclasses

URLSession

URLSessionDataTask

URLSessionConfiguration

Page 45: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

How to Use URLProtocol

URLProtocol subclassesURLProtocol subclassesURLProtocol subclasses

URLSession

URLSessionDataTask

Built-In Protocols (e.g. HTTPS)

URLSessionConfiguration

Page 46: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

How to Use URLProtocol

URLProtocol subclassesURLProtocol subclassesURLProtocol subclasses

URLSession

URLSessionDataTask

Built-In Protocols (e.g. HTTPS)

URLSessionConfiguration

Mock Protocol

Page 47: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

How to Use URLProtocol

URLProtocol subclassesURLProtocol subclassesURLProtocol subclasses

URLSession

URLSessionDataTask

Built-In Protocols (e.g. HTTPS)

URLSessionConfiguration

Mock Protocol

URLProtocolClient

Page 48: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class MockURLProtocol: URLProtocol { override class func canInit(with request: URLRequest) -> Bool { return true }

override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request }

override func startLoading() { // ... }

override func stopLoading() { // ... }}

Page 49: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class MockURLProtocol: URLProtocol { override class func canInit(with request: URLRequest) -> Bool { return true }

override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request }

override func startLoading() { // ... }

override func stopLoading() { // ... }}

Page 50: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class MockURLProtocol: URLProtocol { override class func canInit(with request: URLRequest) -> Bool { return true }

override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request }

override func startLoading() { // ... }

override func stopLoading() { // ... }}

Page 51: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class MockURLProtocol: URLProtocol { override class func canInit(with request: URLRequest) -> Bool { return true }

override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request }

override func startLoading() { // ... }

override func stopLoading() { // ... }}

Page 52: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class MockURLProtocol: URLProtocol { static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? override func startLoading() { guard let handler = MockURLProtocol.requestHandler else { XCTFail("Received unexpected request with no handler set") return } do { let (response, data) = try handler(request) client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } } }

Page 53: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class MockURLProtocol: URLProtocol { static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? override func startLoading() { guard let handler = MockURLProtocol.requestHandler else { XCTFail("Received unexpected request with no handler set") return } do { let (response, data) = try handler(request) client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } } }

Page 54: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class MockURLProtocol: URLProtocol { static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? override func startLoading() { guard let handler = MockURLProtocol.requestHandler else { XCTFail("Received unexpected request with no handler set") return } do { let (response, data) = try handler(request) client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } } }

Page 55: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class MockURLProtocol: URLProtocol { static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? override func startLoading() { guard let handler = MockURLProtocol.requestHandler else { XCTFail("Received unexpected request with no handler set") return } do { let (response, data) = try handler(request) client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } } }

Page 56: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class MockURLProtocol: URLProtocol { static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? override func startLoading() { guard let handler = MockURLProtocol.requestHandler else { XCTFail("Received unexpected request with no handler set") return } do { let (response, data) = try handler(request) client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } } }

Page 57: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class MockURLProtocol: URLProtocol { static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? override func startLoading() { guard let handler = MockURLProtocol.requestHandler else { XCTFail("Received unexpected request with no handler set") return } do { let (response, data) = try handler(request) client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } } }

Page 58: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class MockURLProtocol: URLProtocol { static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? override func startLoading() { guard let handler = MockURLProtocol.requestHandler else { XCTFail("Received unexpected request with no handler set") return } do { let (response, data) = try handler(request) client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } } }

Page 59: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class MockURLProtocol: URLProtocol { static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? override func startLoading() { guard let handler = MockURLProtocol.requestHandler else { XCTFail("Received unexpected request with no handler set") return } do { let (response, data) = try handler(request) client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } } }

Page 60: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class APILoaderTests: XCTestCase { var loader: APIRequestLoader<PointsOfInterestRequest>!

override func setUp() { let request = PointsOfInterestRequest()

let configuration = URLSessionConfiguration.ephemeral configuration.protocolClasses = [MockURLProtocol.self] let urlSession = URLSession(configuration: configuration)

loader = APIRequestLoader(apiRequest: request, urlSession: urlSession) } }

Page 61: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class APILoaderTests: XCTestCase { var loader: APIRequestLoader<PointsOfInterestRequest>!

override func setUp() { let request = PointsOfInterestRequest()

let configuration = URLSessionConfiguration.ephemeral configuration.protocolClasses = [MockURLProtocol.self] let urlSession = URLSession(configuration: configuration)

loader = APIRequestLoader(apiRequest: request, urlSession: urlSession) } }

Page 62: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class APILoaderTests: XCTestCase { var loader: APIRequestLoader<PointsOfInterestRequest>!

override func setUp() { let request = PointsOfInterestRequest()

let configuration = URLSessionConfiguration.ephemeral configuration.protocolClasses = [MockURLProtocol.self] let urlSession = URLSession(configuration: configuration)

loader = APIRequestLoader(apiRequest: request, urlSession: urlSession) } }

Page 63: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class APILoaderTests: XCTestCase { func testLoaderSuccess() { let inputCoordinate = CLLocationCoordinate2D(latitude: 37.3293, longitude: -121.8893) let mockJSONData = "[{\"name\":\"MyPointOfInterest\"}]".data(using: .utf8)! MockURLProtocol.requestHandler = { request in XCTAssertEqual(request.url?.query?.contains("lat=37.3293"), true) return (HTTPURLResponse(), mockJSONData) }

let expectation = XCTestExpectation(description: "response") loader.loadAPIRequest(requestData: inputCoordinate) { pointsOfInterest, error in XCTAssertEqual(pointsOfInterest, [PointOfInterest(name: "MyPointOfInterest")]) expectation.fulfill() } wait(for: [expectation], timeout: 1) } }

Page 64: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class APILoaderTests: XCTestCase { func testLoaderSuccess() { let inputCoordinate = CLLocationCoordinate2D(latitude: 37.3293, longitude: -121.8893) let mockJSONData = "[{\"name\":\"MyPointOfInterest\"}]".data(using: .utf8)! MockURLProtocol.requestHandler = { request in XCTAssertEqual(request.url?.query?.contains("lat=37.3293"), true) return (HTTPURLResponse(), mockJSONData) }

let expectation = XCTestExpectation(description: "response") loader.loadAPIRequest(requestData: inputCoordinate) { pointsOfInterest, error in XCTAssertEqual(pointsOfInterest, [PointOfInterest(name: "MyPointOfInterest")]) expectation.fulfill() } wait(for: [expectation], timeout: 1) } }

Page 65: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class APILoaderTests: XCTestCase { func testLoaderSuccess() { let inputCoordinate = CLLocationCoordinate2D(latitude: 37.3293, longitude: -121.8893) let mockJSONData = "[{\"name\":\"MyPointOfInterest\"}]".data(using: .utf8)! MockURLProtocol.requestHandler = { request in XCTAssertEqual(request.url?.query?.contains("lat=37.3293"), true) return (HTTPURLResponse(), mockJSONData) }

let expectation = XCTestExpectation(description: "response") loader.loadAPIRequest(requestData: inputCoordinate) { pointsOfInterest, error in XCTAssertEqual(pointsOfInterest, [PointOfInterest(name: "MyPointOfInterest")]) expectation.fulfill() } wait(for: [expectation], timeout: 1) } }

Page 66: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class APILoaderTests: XCTestCase { func testLoaderSuccess() { let inputCoordinate = CLLocationCoordinate2D(latitude: 37.3293, longitude: -121.8893) let mockJSONData = "[{\"name\":\"MyPointOfInterest\"}]".data(using: .utf8)! MockURLProtocol.requestHandler = { request in XCTAssertEqual(request.url?.query?.contains("lat=37.3293"), true) return (HTTPURLResponse(), mockJSONData) }

let expectation = XCTestExpectation(description: "response") loader.loadAPIRequest(requestData: inputCoordinate) { pointsOfInterest, error in XCTAssertEqual(pointsOfInterest, [PointOfInterest(name: "MyPointOfInterest")]) expectation.fulfill() } wait(for: [expectation], timeout: 1) } }

Page 67: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class APILoaderTests: XCTestCase { func testLoaderSuccess() { let inputCoordinate = CLLocationCoordinate2D(latitude: 37.3293, longitude: -121.8893) let mockJSONData = "[{\"name\":\"MyPointOfInterest\"}]".data(using: .utf8)! MockURLProtocol.requestHandler = { request in XCTAssertEqual(request.url?.query?.contains("lat=37.3293"), true) return (HTTPURLResponse(), mockJSONData) }

let expectation = XCTestExpectation(description: "response") loader.loadAPIRequest(requestData: inputCoordinate) { pointsOfInterest, error in XCTAssertEqual(pointsOfInterest, [PointOfInterest(name: "MyPointOfInterest")]) expectation.fulfill() } wait(for: [expectation], timeout: 1) } }

Page 68: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class APILoaderTests: XCTestCase { func testLoaderSuccess() { let inputCoordinate = CLLocationCoordinate2D(latitude: 37.3293, longitude: -121.8893) let mockJSONData = "[{\"name\":\"MyPointOfInterest\"}]".data(using: .utf8)! MockURLProtocol.requestHandler = { request in XCTAssertEqual(request.url?.query?.contains("lat=37.3293"), true) return (HTTPURLResponse(), mockJSONData) }

let expectation = XCTestExpectation(description: "response") loader.loadAPIRequest(requestData: inputCoordinate) { pointsOfInterest, error in XCTAssertEqual(pointsOfInterest, [PointOfInterest(name: "MyPointOfInterest")]) expectation.fulfill() } wait(for: [expectation], timeout: 1) } }

Page 69: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class APILoaderTests: XCTestCase { func testLoaderSuccess() { let inputCoordinate = CLLocationCoordinate2D(latitude: 37.3293, longitude: -121.8893) let mockJSONData = "[{\"name\":\"MyPointOfInterest\"}]".data(using: .utf8)! MockURLProtocol.requestHandler = { request in XCTAssertEqual(request.url?.query?.contains("lat=37.3293"), true) return (HTTPURLResponse(), mockJSONData) }

let expectation = XCTestExpectation(description: "response") loader.loadAPIRequest(requestData: inputCoordinate) { pointsOfInterest, error in XCTAssertEqual(pointsOfInterest, [PointOfInterest(name: "MyPointOfInterest")]) expectation.fulfill() } wait(for: [expectation], timeout: 1) } }

Page 70: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Integration Tests

App

App

Server

Update ViewCreate URLSession TaskPrepare URLRequest Parse Response

Page 71: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

App Update ViewCreate URLSession TaskPrepare URLRequest Parse Response

Server

End-to-End Tests

App

Page 72: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

App Update ViewCreate URLSession TaskPrepare URLRequest Parse Response

Server

End-to-End Tests

UI Testing in Xcode WWDC 2015

App

Page 73: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

App Update ViewCreate URLSession TaskPrepare URLRequest Parse Response

Server

End-to-End Tests

App

Page 74: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

App Update ViewCreate URLSession TaskPrepare URLRequest Parse Response

ServerMock Server

End-to-End Tests

App

Page 75: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

End-to-End Tests

App

App

Update ViewCreate URLSession TaskPrepare URLRequest Parse Response

Mock Server

Page 76: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

End-to-End Tests

App

App

Update ViewCreate URLSession TaskPrepare URLRequest Parse Response

Mock ServerServer

Page 77: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Testing Network Requests

Decompose code for testability

URLProtocol as a mocking tool

Tiered testing strategy

Page 78: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

•Testing network requests •Working with notifications •Mocking with protocols •Test execution speed

Page 79: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Working with Notifications

Test that subject properly observes or posts a Notification

Important to test notifications in isolation

Isolation avoids unreliable or “flaky” tests

Page 80: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class PointsOfInterestTableViewController {

var observer: AnyObject?

init() {

let name = CurrentLocationProvider.authChangedNotification

observer = NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in

self?.handleAuthChanged()

}

}

var didHandleNotification = false

func handleAuthChanged() {

didHandleNotification = true

}

}

Page 81: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class PointsOfInterestTableViewController {

var observer: AnyObject?

init() {

let name = CurrentLocationProvider.authChangedNotification

observer = NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in

self?.handleAuthChanged()

}

}

var didHandleNotification = false

func handleAuthChanged() {

didHandleNotification = true

}

}

Page 82: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class PointsOfInterestTableViewController {

var observer: AnyObject?

init() {

let name = CurrentLocationProvider.authChangedNotification

observer = NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in

self?.handleAuthChanged()

}

}

var didHandleNotification = false

func handleAuthChanged() {

didHandleNotification = true

}

}

Page 83: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class PointsOfInterestTableViewController {

var observer: AnyObject?

init() {

let name = CurrentLocationProvider.authChangedNotification

observer = NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in

self?.handleAuthChanged()

}

}

var didHandleNotification = false

func handleAuthChanged() {

didHandleNotification = true

}

}

Page 84: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class PointsOfInterestTableViewControllerTests: XCTestCase {

func testNotification() {

let observer = PointsOfInterestTableViewController()

XCTAssertFalse(observer.didHandleNotification)

// This notification will be received by all objects in process,

// could have unintended consequences

let name = CurrentLocationProvider.authChangedNotification

NotificationCenter.default.post(name: name, object: nil)

XCTAssertTrue(observer.didHandleNotification)

}

}

Page 85: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class PointsOfInterestTableViewControllerTests: XCTestCase {

func testNotification() {

let observer = PointsOfInterestTableViewController()

XCTAssertFalse(observer.didHandleNotification)

// This notification will be received by all objects in process,

// could have unintended consequences

let name = CurrentLocationProvider.authChangedNotification

NotificationCenter.default.post(name: name, object: nil)

XCTAssertTrue(observer.didHandleNotification)

}

}

Page 86: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Testing Notification Observers

How to use • Create separate NotificationCenter, instead of .default • Pass to init() and store in a new property • Replace all uses of NotificationCenter.default with new property

Limits scope of tests by avoiding external effects

Page 87: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class PointsOfInterestTableViewController {

var observer: AnyObject?

init() {

let name = CurrentLocationProvider.authChangedNotification

observer = NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in

self?.handleAuthChanged()

}

}

var didHandleNotification = false

func handleAuthChanged() {

didHandleNotification = true

}

Page 88: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class PointsOfInterestTableViewController {

let notificationCenter: NotificationCenter

var observer: AnyObject?

init(notificationCenter: NotificationCenter) {

self.notificationCenter = notificationCenter

let name = CurrentLocationProvider.authChangedNotification

observer = notificationCenter.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in

self?.handleAuthChanged()

}

}

var didHandleNotification = false

func handleAuthChanged() {

didHandleNotification = true

}

Page 89: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class PointsOfInterestTableViewController {

let notificationCenter: NotificationCenter

var observer: AnyObject?

init(notificationCenter: NotificationCenter = .default) {

self.notificationCenter = notificationCenter

let name = CurrentLocationProvider.authChangedNotification

observer = notificationCenter.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in

self?.handleAuthChanged()

}

}

var didHandleNotification = false

func handleAuthChanged() {

didHandleNotification = true

}

Page 90: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class PointsOfInterestTableViewControllerTests: XCTestCase {

func testNotification() {

let observer = PointsOfInterestTableViewController()

XCTAssertFalse(observer.didHandleNotification)

// This notification will be received by all objects in process,

// could have unintended consequences

let name = CurrentLocationProvider.authChangedNotification

NotificationCenter.default.post(name: name, object: nil)

XCTAssertTrue(observer.didHandleNotification)

}

}

Page 91: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class PointsOfInterestTableViewControllerTests: XCTestCase {

func testNotification() {

let notificationCenter = NotificationCenter()

let observer = PointsOfInterestTableViewController(notificationCenter: notificationCenter)

XCTAssertFalse(observer.didHandleNotification)

// Notification posted to just this center, isolating the test

let name = CurrentLocationProvider.authChangedNotification

notificationCenter.post(name: name, object: nil)

XCTAssertTrue(observer.didHandleNotification)

}

}

Page 92: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Testing Notification Posters

How to validate that a subject posts a Notification?

Use a separate NotificationCenter again

Use XCTNSNotificationExpectation

Page 93: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProvider {

static let authChangedNotification = Notification.Name("AuthChanged")

func notifyAuthChanged() {

let name = CurrentLocationProvider.authChangedNotification

NotificationCenter.default.post(name: name, object: self)

}

}

Page 94: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProvider {

static let authChangedNotification = Notification.Name("AuthChanged")

func notifyAuthChanged() {

let name = CurrentLocationProvider.authChangedNotification

NotificationCenter.default.post(name: name, object: self)

}

}

Page 95: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase { func testNotifyAuthChanged() { let poster = CurrentLocationProvider() var observer: AnyObject? let expectation = self.expectation(description: "auth changed notification")

// Uses default NotificationCenter, not properly isolating test let name = CurrentLocationProvider.authChangedNotification observer = NotificationCenter.default.addObserver(forName: name, object: poster, queue: .main) { _ in NotificationCenter.default.removeObserver(observer!) expectation.fulfill() }

poster.notifyAuthChanged() wait(for: [expectation], timeout: 0) }

Page 96: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase { func testNotifyAuthChanged() { let poster = CurrentLocationProvider() var observer: AnyObject? let expectation = self.expectation(description: "auth changed notification")

// Uses default NotificationCenter, not properly isolating test let name = CurrentLocationProvider.authChangedNotification observer = NotificationCenter.default.addObserver(forName: name, object: poster, queue: .main) { _ in NotificationCenter.default.removeObserver(observer!) expectation.fulfill() }

poster.notifyAuthChanged() wait(for: [expectation], timeout: 0) }

Page 97: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase { func testNotifyAuthChanged() {

let poster = CurrentLocationProvider()

let name = CurrentLocationProvider.authChangedNotification

let expectation = XCTNSNotificationExpectation(name: name, object: poster)

poster.notifyAuthChanged()

wait(for: [expectation], timeout: 0)

}

Page 98: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProvider {

static let authChangedNotification = Notification.Name("AuthChanged")

func notifyAuthChanged() {

let name = CurrentLocationProvider.authChangedNotification

NotificationCenter.default.post(name: name, object: self)

}

}

Page 99: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProvider {

static let authChangedNotification = Notification.Name("AuthChanged")

let notificationCenter: NotificationCenter

init(notificationCenter: NotificationCenter = .default) {

self.notificationCenter = notificationCenter

}

func notifyAuthChanged() {

let name = CurrentLocationProvider.authChangedNotification

notificationCenter.post(name: name, object: self)

}

}

Page 100: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase {

func testNotifyAuthChanged() {

let poster = CurrentLocationProvider()

let name = CurrentLocationProvider.authChangedNotification

let expectation = XCTNSNotificationExpectation(name: name, object: poster)

poster.notifyAuthChanged()

wait(for: [expectation], timeout: 0)

}

}

Page 101: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase {

func testNotifyAuthChanged() {

let notificationCenter = NotificationCenter()

let poster = CurrentLocationProvider(notificationCenter: notificationCenter)

// Notification only sent to this specific center, isolating test

let name = CurrentLocationProvider.authChangedNotification

let expectation = XCTNSNotificationExpectation(name: name, object: poster, notificationCenter: notificationCenter)

poster.notifyAuthChanged()

wait(for: [expectation], timeout: 0)

}

}

Page 102: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase {

func testNotifyAuthChanged() {

let notificationCenter = NotificationCenter()

let poster = CurrentLocationProvider(notificationCenter: notificationCenter)

// Notification only sent to this specific center, isolating test

let name = CurrentLocationProvider.authChangedNotification

let expectation = XCTNSNotificationExpectation(name: name, object: poster, notificationCenter: notificationCenter)

poster.notifyAuthChanged()

wait(for: [expectation], timeout: 0)

}

}

Page 103: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase {

func testNotifyAuthChanged() {

let notificationCenter = NotificationCenter()

let poster = CurrentLocationProvider(notificationCenter: notificationCenter)

// Notification only sent to this specific center, isolating test

let name = CurrentLocationProvider.authChangedNotification

let expectation = XCTNSNotificationExpectation(name: name, object: poster, notificationCenter: notificationCenter)

poster.notifyAuthChanged()

wait(for: [expectation], timeout: 0)

}

}

Page 104: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase {

func testNotifyAuthChanged() {

let notificationCenter = NotificationCenter()

let poster = CurrentLocationProvider(notificationCenter: notificationCenter)

// Notification only sent to this specific center, isolating test

let name = CurrentLocationProvider.authChangedNotification

let expectation = XCTNSNotificationExpectation(name: name, object: poster, notificationCenter: notificationCenter)

poster.notifyAuthChanged()

wait(for: [expectation], timeout: 0)

}

}

Page 105: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

•Testing network requests •Working with notifications •Mocking with protocols •Test execution speed

Page 106: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Mocking with Protocols

Classes often interact with other classes in app or SDK

Many SDK classes cannot be created directly

Delegate protocols make testing more challenging

Solution: Mock interface of external class using protocol

Page 107: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

import CoreLocation

class CurrentLocationProvider: NSObject {

let locationManager = CLLocationManager()

override init() {

super.init()

self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters

self.locationManager.delegate = self

}

// ...

Page 108: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProvider: NSObject {

var currentLocationCheckCallback: ((CLLocation) -> Void)?

func checkCurrentLocation(completion: @escaping (Bool) -> Void) {

self.currentLocationCheckCallback = { [unowned self] location in

completion(self.isPointOfInterest(location))

}

locationManager.requestLocation()

}

func isPointOfInterest(_ location: CLLocation) -> Bool { // Perform check... }

}

Page 109: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProvider: NSObject {

var currentLocationCheckCallback: ((CLLocation) -> Void)?

func checkCurrentLocation(completion: @escaping (Bool) -> Void) {

self.currentLocationCheckCallback = { [unowned self] location in

completion(self.isPointOfInterest(location))

}

locationManager.requestLocation()

}

func isPointOfInterest(_ location: CLLocation) -> Bool { // Perform check... }

}

Page 110: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

extension CurrentLocationProvider: CLLocationManagerDelegate {

func locationManager(_ manager: CLLocationManager, didUpdateLocations locs: [CLLocation]){

guard let location = locs.first else { return }

self.currentLocationCheckCallback?(location)

self.currentLocationCheckCallback = nil

}

}

Page 111: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase {

func testCheckCurrentLocation() {

let provider = CurrentLocationProvider()

XCTAssertNotEqual(provider.locationManager.desiredAccuracy, 0)

XCTAssertNotNil(provider.locationManager.delegate)

let completionExpectation = expectation(description: "completion")

provider.checkCurrentLocation { isPointOfInterest in

XCTAssertTrue(isPointOfInterest)

completionExpectation.fulfill()

}

// No way to mock the current location or confirm requestLocation() was called

wait(for: [completionExpectation], timeout: 1)

} }

Page 112: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase {

func testCheckCurrentLocation() {

let provider = CurrentLocationProvider()

XCTAssertNotEqual(provider.locationManager.desiredAccuracy, 0)

XCTAssertNotNil(provider.locationManager.delegate)

let completionExpectation = expectation(description: "completion")

provider.checkCurrentLocation { isPointOfInterest in

XCTAssertTrue(isPointOfInterest)

completionExpectation.fulfill()

}

// No way to mock the current location or confirm requestLocation() was called

wait(for: [completionExpectation], timeout: 1)

} }

Page 113: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase {

func testCheckCurrentLocation() {

let provider = CurrentLocationProvider()

XCTAssertNotEqual(provider.locationManager.desiredAccuracy, 0)

XCTAssertNotNil(provider.locationManager.delegate)

let completionExpectation = expectation(description: "completion")

provider.checkCurrentLocation { isPointOfInterest in

XCTAssertTrue(isPointOfInterest)

completionExpectation.fulfill()

}

// No way to mock the current location or confirm requestLocation() was called

wait(for: [completionExpectation], timeout: 1)

} }

Page 114: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase {

func testCheckCurrentLocation() {

let provider = CurrentLocationProvider()

XCTAssertNotEqual(provider.locationManager.desiredAccuracy, 0)

XCTAssertNotNil(provider.locationManager.delegate)

let completionExpectation = expectation(description: "completion")

provider.checkCurrentLocation { isPointOfInterest in

XCTAssertTrue(isPointOfInterest)

completionExpectation.fulfill()

}

// No way to mock the current location or confirm requestLocation() was called

wait(for: [completionExpectation], timeout: 1)

} }

Page 115: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase {

func testCheckCurrentLocation() {

let provider = CurrentLocationProvider()

XCTAssertNotEqual(provider.locationManager.desiredAccuracy, 0)

XCTAssertNotNil(provider.locationManager.delegate)

let completionExpectation = expectation(description: "completion")

provider.checkCurrentLocation { isPointOfInterest in

XCTAssertTrue(isPointOfInterest)

completionExpectation.fulfill()

}

// No way to mock the current location or confirm requestLocation() was called

wait(for: [completionExpectation], timeout: 1)

} }

Page 116: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Mocking Using a Subclass

Subclassing the external class in tests and overriding methods

Can work, but risky

Some SDK classes cannot be subclassed

Easy to forget to override methods

Page 117: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Mocking Using a Subclass

Subclassing the external class in tests and overriding methods

Can work, but risky

Some SDK classes cannot be subclassed

Easy to forget to override methods

Page 118: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProvider: NSObject {

let locationManager = CLLocationManager()

override init() {

super.init()

self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters

self.locationManager.delegate = self

} }

Page 119: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProvider: NSObject {

let locationManager = CLLocationManager()

override init() {

super.init()

self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters

self.locationManager.delegate = self

} }

protocol LocationFetcher {

var delegate: CLLocationManagerDelegate? { get set }

var desiredAccuracy: CLLocationAccuracy { get set }

func requestLocation()

} extension CLLocationManager: LocationFetcher {}

Page 120: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProvider: NSObject { var locationFetcher: LocationFetcher

init(locationFetcher: LocationFetcher) {

self.locationFetcher = locationFetcher

super.init()

self.locationFetcher.desiredAccuracy = kCLLocationAccuracyHundredMeters

self.locationFetcher.delegate = self

}

}

protocol LocationFetcher {

var delegate: CLLocationManagerDelegate? { get set }

var desiredAccuracy: CLLocationAccuracy { get set }

func requestLocation()

} extension CLLocationManager: LocationFetcher {}

Page 121: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProvider: NSObject { var locationFetcher: LocationFetcher

init(locationFetcher: LocationFetcher = CLLocationManager()) {

self.locationFetcher = locationFetcher

super.init()

self.locationFetcher.desiredAccuracy = kCLLocationAccuracyHundredMeters

self.locationFetcher.delegate = self

}

}

protocol LocationFetcher {

var delegate: CLLocationManagerDelegate? { get set }

var desiredAccuracy: CLLocationAccuracy { get set }

func requestLocation()

} extension CLLocationManager: LocationFetcher {}

Page 122: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProvider: NSObject {

var currentLocationCheckCallback: ((CLLocation) -> Void)?

func checkCurrentLocation(completion: @escaping (Bool) -> Void) {

self.currentLocationCheckCallback = { [unowned self] location in

completion(self.isPointOfInterest(location))

}

locationManager.requestLocation()

}

func isPointOfInterest(_ location: CLLocation) -> Bool { // Perform check... }

}

Page 123: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProvider: NSObject {

var currentLocationCheckCallback: ((CLLocation) -> Void)?

func checkCurrentLocation(completion: @escaping (Bool) -> Void) {

self.currentLocationCheckCallback = { [unowned self] location in

completion(self.isPointOfInterest(location))

}

locationFetcher.requestLocation()

}

func isPointOfInterest(_ location: CLLocation) -> Bool { // Perform check... }

}

Page 124: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

extension CurrentLocationProvider: CLLocationManagerDelegate {

func locationManager(_ manager: CLLocationManager, didUpdateLocations locs: [CLLocation]){

guard let location = locs.first else { return }

self.currentLocationCheckCallback?(location)

self.currentLocationCheckCallback = nil

}

}

Page 125: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

protocol LocationFetcher {

var delegate: CLLocationManagerDelegate? { get set } // ...

}

extension CLLocationManager: LocationFetcher {}

Page 126: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

protocol LocationFetcher { var locationFetcherDelegate: LocationFetcherDelegate? { get set } // ... } protocol LocationFetcherDelegate: class { func locationFetcher(_ fetcher: LocationFetcher, didUpdateLocations locs: [CLLocation]) }

extension CLLocationManager: LocationFetcher {}

Page 127: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

protocol LocationFetcher { var locationFetcherDelegate: LocationFetcherDelegate? { get set } // ... } protocol LocationFetcherDelegate: class { func locationFetcher(_ fetcher: LocationFetcher, didUpdateLocations locs: [CLLocation]) }

extension CLLocationManager: LocationFetcher { var locationFetcherDelegate: LocationFetcherDelegate? { get { return delegate as! LocationFetcherDelegate? } set { delegate = newValue as! CLLocationManagerDelegate? }

} }

Page 128: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

protocol LocationFetcher { var locationFetcherDelegate: LocationFetcherDelegate? { get set } // ... } protocol LocationFetcherDelegate: class { func locationFetcher(_ fetcher: LocationFetcher, didUpdateLocations locs: [CLLocation]) }

class CurrentLocationProvider: NSObject { var locationFetcher: LocationFetcher

init(locationFetcher: LocationFetcher = CLLocationManager()) {

// ... self.locationFetcher.delegate = self

}

}

Page 129: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

protocol LocationFetcher { var locationFetcherDelegate: LocationFetcherDelegate? { get set } // ... } protocol LocationFetcherDelegate: class { func locationFetcher(_ fetcher: LocationFetcher, didUpdateLocations locs: [CLLocation]) }

class CurrentLocationProvider: NSObject { var locationFetcher: LocationFetcher

init(locationFetcher: LocationFetcher = CLLocationManager()) {

// ... self.locationFetcher.locationFetcherDelegate = self

}

}

Page 130: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

extension CurrentLocationProvider: CLLocationManagerDelegate {

func locationManager(_ manager: CLLocationManager, didUpdateLocations locs: [CLLocation]){

guard let location = locs.first else { return }

self.currentLocationCheckCallback?(location)

self.currentLocationCheckCallback = nil

}

}

Page 131: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

extension CurrentLocationProvider: LocationFetcherDelegate {

func locationFetcher(_ fetcher: LocationFetcher, didUpdateLocations locs: [CLLocation]){

guard let location = locs.first else { return }

self.currentLocationCheckCallback?(location)

self.currentLocationCheckCallback = nil

}

}

Page 132: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

extension CurrentLocationProvider: LocationFetcherDelegate {

func locationFetcher(_ fetcher: LocationFetcher, didUpdateLocations locs: [CLLocation]){

guard let location = locs.first else { return }

self.currentLocationCheckCallback?(location)

self.currentLocationCheckCallback = nil

}

}

extension CurrentLocationProvider: CLLocationManagerDelegate {

func locationManager(_ manager: CLLocationManager, didUpdateLocations locs: [CLLocation]){

self.locationFetcher(manager, didUpdateLocations: locs)

}

}

Page 133: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

extension CurrentLocationProvider: LocationFetcherDelegate {

func locationFetcher(_ fetcher: LocationFetcher, didUpdateLocations locs: [CLLocation]){

guard let location = locs.first else { return }

self.currentLocationCheckCallback?(location)

self.currentLocationCheckCallback = nil

}

}

extension CurrentLocationProvider: CLLocationManagerDelegate {

func locationManager(_ manager: CLLocationManager, didUpdateLocations locs: [CLLocation]){

self.locationFetcher(manager, didUpdateLocations: locs)

}

}

Page 134: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase {

struct MockLocationFetcher: LocationFetcher {

weak var locationFetcherDelegate: LocationFetcherDelegate?

var desiredAccuracy: CLLocationAccuracy = 0

var handleRequestLocation: (() -> CLLocation)?

func requestLocation() {

guard let location = handleRequestLocation?() else { return }

locationFetcherDelegate?.locationFetcher(self, didUpdateLocations: [location])

}

}

Page 135: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase {

struct MockLocationFetcher: LocationFetcher {

weak var locationFetcherDelegate: LocationFetcherDelegate?

var desiredAccuracy: CLLocationAccuracy = 0

var handleRequestLocation: (() -> CLLocation)?

func requestLocation() {

guard let location = handleRequestLocation?() else { return }

locationFetcherDelegate?.locationFetcher(self, didUpdateLocations: [location])

}

}

Page 136: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class CurrentLocationProviderTests: XCTestCase {

struct MockLocationFetcher: LocationFetcher {

weak var locationFetcherDelegate: LocationFetcherDelegate?

var desiredAccuracy: CLLocationAccuracy = 0

var handleRequestLocation: (() -> CLLocation)?

func requestLocation() {

guard let location = handleRequestLocation?() else { return }

locationFetcherDelegate?.locationFetcher(self, didUpdateLocations: [location])

}

}

Page 137: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

func testCheckCurrentLocation() {

var locationFetcher = MockLocationFetcher()

let requestLocationExpectation = expectation(description: "request location")

locationFetcher.handleRequestLocation = {

requestLocationExpectation.fulfill()

return CLLocation(latitude: 37.3293, longitude: -121.8893)

}

let provider = CurrentLocationProvider(locationFetcher: locationFetcher)

let completionExpectation = expectation(description: "completion")

provider.checkCurrentLocation { isPointOfInterest in

XCTAssertTrue(isPointOfInterest)

completionExpectation.fulfill()

}

// Can mock the current location

wait(for: [requestLocationExpectation, completionExpectation], timeout: 1)

}

Page 138: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

func testCheckCurrentLocation() {

var locationFetcher = MockLocationFetcher()

let requestLocationExpectation = expectation(description: "request location")

locationFetcher.handleRequestLocation = {

requestLocationExpectation.fulfill()

return CLLocation(latitude: 37.3293, longitude: -121.8893)

}

let provider = CurrentLocationProvider(locationFetcher: locationFetcher)

let completionExpectation = expectation(description: "completion")

provider.checkCurrentLocation { isPointOfInterest in

XCTAssertTrue(isPointOfInterest)

completionExpectation.fulfill()

}

// Can mock the current location

wait(for: [requestLocationExpectation, completionExpectation], timeout: 1)

}

Page 139: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

func testCheckCurrentLocation() {

var locationFetcher = MockLocationFetcher()

let requestLocationExpectation = expectation(description: "request location")

locationFetcher.handleRequestLocation = {

requestLocationExpectation.fulfill()

return CLLocation(latitude: 37.3293, longitude: -121.8893)

}

let provider = CurrentLocationProvider(locationFetcher: locationFetcher)

let completionExpectation = expectation(description: "completion")

provider.checkCurrentLocation { isPointOfInterest in

XCTAssertTrue(isPointOfInterest)

completionExpectation.fulfill()

}

// Can mock the current location

wait(for: [requestLocationExpectation, completionExpectation], timeout: 1)

}

Page 140: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

func testCheckCurrentLocation() {

var locationFetcher = MockLocationFetcher()

let requestLocationExpectation = expectation(description: "request location")

locationFetcher.handleRequestLocation = {

requestLocationExpectation.fulfill()

return CLLocation(latitude: 37.3293, longitude: -121.8893)

}

let provider = CurrentLocationProvider(locationFetcher: locationFetcher)

let completionExpectation = expectation(description: "completion")

provider.checkCurrentLocation { isPointOfInterest in

XCTAssertTrue(isPointOfInterest)

completionExpectation.fulfill()

}

// Can mock the current location

wait(for: [requestLocationExpectation, completionExpectation], timeout: 1)

}

Page 141: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

func testCheckCurrentLocation() {

var locationFetcher = MockLocationFetcher()

let requestLocationExpectation = expectation(description: "request location")

locationFetcher.handleRequestLocation = {

requestLocationExpectation.fulfill()

return CLLocation(latitude: 37.3293, longitude: -121.8893)

}

let provider = CurrentLocationProvider(locationFetcher: locationFetcher)

let completionExpectation = expectation(description: "completion")

provider.checkCurrentLocation { isPointOfInterest in

XCTAssertTrue(isPointOfInterest)

completionExpectation.fulfill()

}

// Can mock the current location

wait(for: [requestLocationExpectation, completionExpectation], timeout: 1)

}

Page 142: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

func testCheckCurrentLocation() {

var locationFetcher = MockLocationFetcher()

let requestLocationExpectation = expectation(description: "request location")

locationFetcher.handleRequestLocation = {

requestLocationExpectation.fulfill()

return CLLocation(latitude: 37.3293, longitude: -121.8893)

}

let provider = CurrentLocationProvider(locationFetcher: locationFetcher)

let completionExpectation = expectation(description: "completion")

provider.checkCurrentLocation { isPointOfInterest in

XCTAssertTrue(isPointOfInterest)

completionExpectation.fulfill()

}

// Can mock the current location

wait(for: [requestLocationExpectation, completionExpectation], timeout: 1)

}

Page 143: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

func testCheckCurrentLocation() {

var locationFetcher = MockLocationFetcher()

let requestLocationExpectation = expectation(description: "request location")

locationFetcher.handleRequestLocation = {

requestLocationExpectation.fulfill()

return CLLocation(latitude: 37.3293, longitude: -121.8893)

}

let provider = CurrentLocationProvider(locationFetcher: locationFetcher)

let completionExpectation = expectation(description: "completion")

provider.checkCurrentLocation { isPointOfInterest in

XCTAssertTrue(isPointOfInterest)

completionExpectation.fulfill()

}

// Can mock the current location

wait(for: [requestLocationExpectation, completionExpectation], timeout: 1)

}

Page 144: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Mocking with Protocols Recap

Define a protocol representing the external interface

Define extension on the external class conforming to the protocol

Replace all usage of external class with the protocol

Set the external reference via initializer or a property, using the protocol type

Page 145: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Mocking Delegates with Protocols Recap

Define delegate protocol with interfaces your code implements

Replace subject type with mock protocol defined earlier

In mock protocol, rename delegate property

In extension on original type, implement mock delegate property and convert

Page 146: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

•Testing network requests •Working with notifications •Mocking with protocols •Test execution speed

Page 147: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Test Execution Speed

Slow tests hinder developer productivity

Want tests to run fast

Artificial delays should not be necessary with sufficient mocking

Page 148: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManager {

var currentPlace: Place

func scheduleNextPlace() {

// Show next place after 10 seconds

Timer.scheduledTimer(withTimeInterval: 10, repeats: false) { [weak self] _ in

self?.showNextPlace()

}

}

func showNextPlace() {

// Set currentPlace to next place...

}

}

Page 149: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManagerTests: XCTestCase {

func testScheduleNextPlace() {

let manager = FeaturedPlaceManager()

let beforePlace = manager.currentPlace

manager.scheduleNextPlace()

// Slow! Not ideal

RunLoop.current.run(until: Date(timeIntervalSinceNow: 11))

XCTAssertNotEqual(manager.currentPlace, beforePlace)

}

}

Page 150: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManager {

var currentPlace: Place

func scheduleNextPlace() {

Timer.scheduledTimer(withTimeInterval: 10, repeats: false) { [weak self] _ in

self?.showNextPlace()

}

}

func showNextPlace() {

// Set currentPlace to next place...

}

}

Page 151: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManager {

var currentPlace: Place

var interval: TimeInterval = 10

func scheduleNextPlace() {

Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in

self?.showNextPlace()

}

}

func showNextPlace() {

// Set currentPlace to next place...

}

}

Page 152: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManagerTests: XCTestCase {

func testScheduleNextPlace() {

let manager = FeaturedPlaceManager()

manager.interval = 1

let beforePlace = manager.currentPlace

manager.scheduleNextPlace()

// Less slow but still timing-dependent

RunLoop.current.run(until: Date(timeIntervalSinceNow: 2))

XCTAssertNotEqual(manager.currentPlace, beforePlace)

}

}

Page 153: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Testing Delayed Actions Without the delay

How to use • Identify the “delay” technique (e.g. Timer, DispatchQueue.asyncAfter) • Mock this mechanism during tests • Invoke delayed action immediately

Page 154: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManager {

func scheduleNextPlace() {

Timer.scheduledTimer(withTimeInterval: 10, repeats: false) { [weak self] _ in

self?.showNextPlace()

}

}

func showNextPlace() { /* ... */ }

}

Page 155: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManager {

func scheduleNextPlace() {

Timer.scheduledTimer(withTimeInterval: 10, repeats: false) { [weak self] _ in

self?.showNextPlace()

}

}

func showNextPlace() { /* ... */ }

}

Page 156: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManager {

let runLoop = RunLoop.current

func scheduleNextPlace() {

let timer = Timer(timeInterval: 10, repeats: false) { [weak self] _ in

self?.showNextPlace()

}

runLoop.add(timer, forMode: .default)

}

func showNextPlace() { /* ... */ }

}

Page 157: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

protocol TimerScheduler {

func add(_ timer: Timer, forMode mode: RunLoop.Mode)

}

extension RunLoop: TimerScheduler {}

Page 158: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManager {

let runLoop = RunLoop.current

func scheduleNextPlace() {

let timer = Timer(timeInterval: 10, repeats: false) { [weak self] _ in

self?.showNextPlace()

}

runLoop.add(timer, forMode: .default)

}

func showNextPlace() { /* ... */ }

}

Page 159: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManager {

let timerScheduler: TimerScheduler

init(timerScheduler: TimerScheduler) {

self.timerScheduler = timerScheduler

}

func scheduleNextPlace() {

let timer = Timer(timeInterval: 10, repeats: false) { [weak self] _ in

self?.showNextPlace()

}

timerScheduler.add(timer, forMode: .default)

}

func showNextPlace() { /* ... */ }

}

Page 160: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManagerTests: XCTestCase {

struct MockTimerScheduler: TimerScheduler {

var handleAddTimer: ((_ timer: Timer) -> Void)?

func add(_ timer: Timer, forMode mode: RunLoop.Mode) {

handleAddTimer?(timer)

}

}

// ...

Page 161: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManagerTests: XCTestCase {

func testScheduleNextPlace() {

var timerScheduler = MockTimerScheduler()

var timerDelay = TimeInterval(0)

timerScheduler.handleAddTimer = { timer in

timerDelay = timer.fireDate.timeIntervalSinceNow

timer.fire()

}

let manager = FeaturedPlaceManager(timerScheduler: timerScheduler)

let beforePlace = manager.currentPlace

manager.scheduleNextPlace()

// No delay!

XCTAssertEqual(timerDelay, 10, accuracy: 1)

XCTAssertNotEqual(manager.currentPlace, beforePlace)

}

Page 162: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManagerTests: XCTestCase {

func testScheduleNextPlace() {

var timerScheduler = MockTimerScheduler()

var timerDelay = TimeInterval(0)

timerScheduler.handleAddTimer = { timer in

timerDelay = timer.fireDate.timeIntervalSinceNow

timer.fire()

}

let manager = FeaturedPlaceManager(timerScheduler: timerScheduler)

let beforePlace = manager.currentPlace

manager.scheduleNextPlace()

// No delay!

XCTAssertEqual(timerDelay, 10, accuracy: 1)

XCTAssertNotEqual(manager.currentPlace, beforePlace)

}

Page 163: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManagerTests: XCTestCase {

func testScheduleNextPlace() {

var timerScheduler = MockTimerScheduler()

var timerDelay = TimeInterval(0)

timerScheduler.handleAddTimer = { timer in

timerDelay = timer.fireDate.timeIntervalSinceNow

timer.fire()

}

let manager = FeaturedPlaceManager(timerScheduler: timerScheduler)

let beforePlace = manager.currentPlace

manager.scheduleNextPlace()

// No delay!

XCTAssertEqual(timerDelay, 10, accuracy: 1)

XCTAssertNotEqual(manager.currentPlace, beforePlace)

}

Page 164: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManagerTests: XCTestCase {

func testScheduleNextPlace() {

var timerScheduler = MockTimerScheduler()

var timerDelay = TimeInterval(0)

timerScheduler.handleAddTimer = { timer in

timerDelay = timer.fireDate.timeIntervalSinceNow

timer.fire()

}

let manager = FeaturedPlaceManager(timerScheduler: timerScheduler)

let beforePlace = manager.currentPlace

manager.scheduleNextPlace()

// No delay!

XCTAssertEqual(timerDelay, 10, accuracy: 1)

XCTAssertNotEqual(manager.currentPlace, beforePlace)

}

Page 165: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManagerTests: XCTestCase {

func testScheduleNextPlace() {

var timerScheduler = MockTimerScheduler()

var timerDelay = TimeInterval(0)

timerScheduler.handleAddTimer = { timer in

timerDelay = timer.fireDate.timeIntervalSinceNow

timer.fire()

}

let manager = FeaturedPlaceManager(timerScheduler: timerScheduler)

let beforePlace = manager.currentPlace

manager.scheduleNextPlace()

// No delay!

XCTAssertEqual(timerDelay, 10, accuracy: 1)

XCTAssertNotEqual(manager.currentPlace, beforePlace)

}

Page 166: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

class FeaturedPlaceManagerTests: XCTestCase {

func testScheduleNextPlace() {

var timerScheduler = MockTimerScheduler()

var timerDelay = TimeInterval(0)

timerScheduler.handleAddTimer = { timer in

timerDelay = timer.fireDate.timeIntervalSinceNow

timer.fire()

}

let manager = FeaturedPlaceManager(timerScheduler: timerScheduler)

let beforePlace = manager.currentPlace

manager.scheduleNextPlace()

// No delay!

XCTAssertEqual(timerDelay, 10, accuracy: 1)

XCTAssertNotEqual(manager.currentPlace, beforePlace)

}

Page 167: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Testing Delayed Actions

Delay is eliminated using this technique

Majority of tests should be direct, without mocking delays

Page 168: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Setting Expectations

Be mindful when using XCTNSPredicateExpectation

Slower and best suited for UI tests

Use faster, callback-based expectations in unit tests: • XCTestExpectation • XCTNSNotificationExpectation • XCTKVOExpectation

Page 169: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Optimizing App Launch When Testing

Avoid unnecessary work when app is launched as unit test host

Testing begins after ‘app did finish launching’

Set custom scheme environment variables/launch arguments for testing

Page 170: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Optimizing App Launch When Testing

Page 171: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Optimizing App Launch When Testing

func application(_ application: UIApplication, didFinishLaunchingWithOptions opts: …) -> Bool {

let isUnitTesting = ProcessInfo.processInfo.environment["IS_UNIT_TESTING"] == "1"

if isUnitTesting == false {

// Do UI-related setup, which can be skipped when testing

}

return true

}

Page 172: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

Summary

Testing network requests

Working with notifications

Mocking with protocols

Test execution speed

Page 173: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests

More Informationhttps://developer.apple.com/wwdc18/417

Page 174: Testing Tips & Tricks€¦ · •Testing network requests • Working with notifications • Mocking with protocols • Test execution speed • Testing network requests