XCTest
NSURLSession
main thread
iOS development
concurrency issues

XCTest NSURLSession Stall on main thread

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

NSURLSession tests can appear to stall when the test blocks the main thread or waits incorrectly for asynchronous callbacks. XCTest is designed to coordinate async work through expectations or async test functions, not through manual run-loop blocking. This guide shows patterns that keep networking tests deterministic and fast.

Why Main-Thread Stalls Happen

A common anti-pattern is waiting with semaphores on the main thread while URLSession callbacks also need main-thread execution context. This creates a deadlock-like situation where the callback cannot run, so the test never finishes.

swift
1// Avoid this pattern in tests
2let semaphore = DispatchSemaphore(value: 0)
3URLSession.shared.dataTask(with: url) { _, _, _ in
4    semaphore.signal()
5}.resume()
6semaphore.wait() // Risky on main test thread

Instead, use XCTest expectations or async test methods.

Correct XCTestExpectation Pattern

Expectations make callback completion explicit and integrate with XCTest timeouts.

swift
1import XCTest
2
3final class APITests: XCTestCase {
4    func testFetchUser() {
5        let expectation = expectation(description: "Fetch user response")
6        let url = URL(string: "https://httpbin.org/json")!
7
8        URLSession.shared.dataTask(with: url) { data, response, error in
9            XCTAssertNil(error)
10            XCTAssertNotNil(data)
11            XCTAssertNotNil(response)
12            expectation.fulfill()
13        }.resume()
14
15        waitForExpectations(timeout: 5)
16    }
17}

This keeps the test runner responsive and provides clear failure messages on timeout.

Modern Async Test Functions

On modern Swift and XCTest versions, async test functions are cleaner.

swift
1import XCTest
2
3final class AsyncAPITests: XCTestCase {
4    func testFetchStatusCode() async throws {
5        let url = URL(string: "https://httpbin.org/status/200")!
6        let (_, response) = try await URLSession.shared.data(from: url)
7
8        guard let http = response as? HTTPURLResponse else {
9            XCTFail("Expected HTTP response")
10            return
11        }
12
13        XCTAssertEqual(http.statusCode, 200)
14    }
15}

This avoids manual expectation bookkeeping and reads like synchronous code.

Isolate Networking for Reliable Unit Tests

For true unit tests, avoid real network calls and inject a custom URLProtocol mock. This removes flakiness from external services.

swift
1import Foundation
2
3final class MockURLProtocol: URLProtocol {
4    static var responseData: Data?
5    static var response: URLResponse?
6    static var responseError: Error?
7
8    override class func canInit(with request: URLRequest) -> Bool { true }
9    override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
10
11    override func startLoading() {
12        if let response = Self.response {
13            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
14        }
15        if let data = Self.responseData {
16            client?.urlProtocol(self, didLoad: data)
17        }
18        if let error = Self.responseError {
19            client?.urlProtocol(self, didFailWithError: error)
20        } else {
21            client?.urlProtocolDidFinishLoading(self)
22        }
23    }
24
25    override func stopLoading() {}
26}

Use this with a dedicated URLSessionConfiguration in tests for predictable outcomes.

Test Delegate-Based Sessions Safely

If your production networking layer uses URLSessionDelegate, keep tests asynchronous and isolate delegate callbacks with dedicated expectations.

swift
1import XCTest
2
3final class DelegateAPITests: XCTestCase, URLSessionDataDelegate {
4    var didReceiveData = false
5
6    func testDelegateCallback() {
7        let exp = expectation(description: "delegate callback")
8        let config = URLSessionConfiguration.ephemeral
9        let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
10        let url = URL(string: "https://httpbin.org/bytes/8")!
11
12        session.dataTask(with: url) { _, _, error in
13            XCTAssertNil(error)
14            exp.fulfill()
15        }.resume()
16
17        waitForExpectations(timeout: 5)
18        XCTAssertTrue(didReceiveData)
19    }
20
21    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
22        didReceiveData = true
23    }
24}

This keeps callback delivery observable without blocking the thread that drives test execution.

Keep Integration Tests Separate

Network integration tests and unit tests should not share the same reliability expectations. Mark slow network tests clearly and run them in a dedicated pipeline stage. Fast deterministic unit tests should rely on mocks, while integration tests verify endpoint contracts and authentication behavior.

Common Pitfalls

The biggest pitfall is mixing asynchronous APIs with synchronous waiting primitives on the test thread. Another issue is setting timeouts too low for CI machines, causing random failures even when code is correct. Teams also often leave real network dependencies in unit tests, which introduces nondeterministic failures from DNS, latency, or remote outages. Finally, avoid fulfilling expectations multiple times from retry logic unless the test is designed for it. XCTest will flag over-fulfillment as a failure.

Summary

  • Do not block the main test thread with semaphores for URLSession callbacks.
  • Use XCTest expectations or async test functions for network completion.
  • Prefer mocked networking for deterministic unit tests.
  • Set realistic timeouts and clear assertions for CI stability.
  • Keep async test flow explicit to prevent hidden deadlocks.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.