Testing iOS Apps — Integration Tests with 3rd Party
A guide to mocking external dependencies and using protocol-oriented programming for robust iOS integration testing.
Testing integration between your code and external libraries presents unique challenges. Several categories of dependencies require special handling:
Hardware API Libraries:
- CoreLocation (geolocation and movement)
- AVFoundation (audio/visual access)
- HealthKit (user health data)
- URLSession (network communication)
External Libraries:
- Firebase Analytics
- Realm (data persistence)
- Alamofire (HTTP networking)
Protocol-Oriented Programming Fundamentals
Protocols in Swift function similarly to interfaces in other languages — they define contracts that implementing types must follow. This approach enables:
- Polymorphism: Different classes responding identically to the same function calls
- Multiple Inheritance: Single classes implementing multiple protocols simultaneously
Creating Robust Mocks Through Wrapper Pattern
Rather than mocking external dependencies directly, the recommended approach uses a Wrapper — an abstraction layer around third-party functionality.
NetworkSession Protocol
protocol NetworkSession {
func fetchData(from url: URL) async throws -> (Data, URLResponse)
}
MockNetworkSession Implementation
class MockNetworkSession: NetworkSession {
var data: Data?
var response: URLResponse?
var error: Error?
func fetchData(from url: URL) async throws -> (Data, URLResponse) {
if let error = error {
throw error
}
guard let data = data, let response = response else {
throw URLError(.badServerResponse)
}
return (data, response)
}
}
This mock supports three testing scenarios:
- Simulating failures with custom errors
- Returning correct data responses
- Simulating different HTTP status codes
Testing Integration
func testFetchWeatherSuccess() async throws {
let mockSession = MockNetworkSession()
mockSession.data = """
{
"id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"city": "San Francisco",
"weatherCondition": "Sunny",
"date": "2024-06-12T12:00:00Z"
}
""".data(using: .utf8)
mockSession.response = HTTPURLResponse(
url: url,
statusCode: 200,
httpVersion: nil,
headerFields: nil
)
let service = WeatherService(session: mockSession)
let weather = try await service.fetchWeather(for: url)
XCTAssertEqual(weather.city, "San Francisco")
XCTAssertEqual(weather.weatherCondition, "Sunny")
}
func testFetchWeatherFail() async throws {
let mockSession = MockNetworkSession()
mockSession.error = URLError(.badURL)
let service = WeatherService(session: mockSession)
do {
let weather = try await service.fetchWeather(for: url)
XCTFail("This should fail")
} catch (let e) {
XCTAssertNotNil(e)
}
}
Service Implementation
class WeatherService {
private let session: NetworkSession
init(session: NetworkSession) {
self.session = session
}
func fetchWeather(for url: URL) async throws -> Weather {
let (data, response) = try await session.fetchData(from: url)
let weather = try JSONDecoder().decode(Weather.self, from: data)
return weather
}
}
URLSession Wrapper
class URLSessionWrapper: NetworkSession {
private let session: URLSession
init(session: URLSession = .shared) {
self.session = session
}
func fetchData(from url: URL) async throws -> (Data, URLResponse) {
return try await session.data(for: URLRequest(url: url))
}
}
Flexibility and Maintainability Benefits
Should your team need to replace URLSession with Alamofire, the wrapper pattern minimizes impact. You simply create a new wrapper implementation:
import Alamofire
class AlamofireSessionWrapper: NetworkSession {
func fetchData(from url: URL) async throws -> (Data, URLResponse) {
return try await withCheckedThrowingContinuation { continuation in
AF.request(url).validate().responseData { response in
switch response.result {
case .success(let data):
if let httpResponse = response.response {
continuation.resume(returning: (data, httpResponse))
} else {
continuation.resume(throwing: URLError(.badServerResponse))
}
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
}
This approach maintains the same interface, ensuring your business logic remains unchanged and adaptable to library modifications or replacements.