Modern Concurrency: Beyond the Basics

Oct 20 2022 · Swift 5.5, iOS 15, Xcode 13.4

Part 1: AsyncStream & Continuations

06. Unit Testing

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 05. Using a Buffered AsyncStream Next episode: 07. Wrapping Delegate With Continuation

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Heads up... You've reached locked video content where the transcript will be shown as obfuscated text.

Refresh your browser to make sure the course server is running or restart the server in Terminal. Continue with your project from the previous episode or open the starter project for this episode.

Capturing network calls under test

In this episode, you’ll add tests for the BlabberModel type. To verify that BlabberModel sends correct data to the server, you’ll configure a custom URLSession for your tests to work with. You’ll intercept and record all network requests using a custom URLProtocol subclass.

Implementing a custom URLProtocol

In the BlabberTests Utility group, open TestURLProtocol.swift:

startLoading()

The starter code in startLoading() creates a successful server response with no content and returns it to the client. For these tests, you’re only interested in the outgoing requests, not what comes back from the server. You’ll also record the network requests here.

static var lastRequest: URLRequest?
guard let stream = request.httpBodyStream else {
  fatalError("Unexpected test scenario")
}
guard let stream = request.httpBodyStream else {
  fatalError("Unexpected test scenario")
}
🟩
var request = request
guard let stream = request.httpBodyStream else {
  fatalError("Unexpected test scenario")
}

var request = request
🟩
request.httpBody = stream.data
guard let stream = request.httpBodyStream else {
  fatalError("Unexpected test scenario")
}

var request = request
request.httpBody = stream.data
🟩
Self.lastRequest = request

Creating a model for testing

In BlabberTests, create a model property …

let model: BlabberModel
let model: BlabberModel🟩 = {  

}()
let model: BlabberModel = {
  🟩
  // First, create a new `BlabberModel` with username test
  let model = BlabberModel()
  model.username = "test"

  // Then create a URL session configuration that uses `TestURLProtocol`
  let testConfiguration = URLSessionConfiguration.default
  testConfiguration.protocolClasses = [TestURLProtocol.self]

  // And tell the model to use this new session
  model.urlSession = URLSession(configuration: testConfiguration)
  // And return the model
  return model
  🟥
}()

Adding a simple asynchronous test

And finally, write your first test!

func testModelSay() async throws {  // say first paragraph below
  try await model.say("Hello!")  

}
// first, unwrap the optional TestURLProtocol.lastRequest
let request = try XCTUnwrap(TestURLProtocol.lastRequest)

// then check the URL matches the expected address
XCTAssertEqual(
  request.url?.absoluteString, "http://localhost:8080/chat/say"
)
// first, unwrap the request body
let httpBody = try XCTUnwrap(request.httpBody)
// then decode the request body: it should decode as a Message
let message = try XCTUnwrap(try? JSONDecoder()
  .decode(Message.self, from: httpBody))
// and the decoded Message should be "Hello!"
XCTAssertEqual(message.message, "Hello!")

Testing values over time with AsyncStream

That was easy. Now, how to test asynchronous work that may yield many values, like countdown? This sequence requires up to 4 network requests. To guarantee the method works correctly, you must verify more than the last value.

// add a static property holding a continuation
static private var continuation: AsyncStream<URLRequest>.Continuation?

// add a static property that returns an asynchronous stream that emits requests
static var requests: AsyncStream<URLRequest> = {
  AsyncStream { continuation in
  // store the AsyncStream's continuation so you can 
  // emit a value each time `TestURLProtocol` responds to a request
    TestURLProtocol.continuation = continuation
  }
}()
static var lastRequest: URLRequest? 🟩{
  didSet {
    if let request = lastRequest {
      continuation?.yield(request)
    }
  }
}
func testModelCountdown() async throws {
  // call countdown
  try await model.countdown(to: "Tada!")
  // iterate over the stream of requests to print the recorded values
  for await request in TestURLProtocol.requests {
    print(request)
  }
}
🟩async let countdown: Void =🟥 model.countdown(to: "Tada!")
async let countdown: Void = model.countdown(to: "Tada!")
🟩async let messages = 🟥TestURLProtocol.requests // and delete the for await closure
let (messagesResult, _) = try await (messages, countdown)
XCTAssertEqual(
  ["3...", "2...", "1...", "🎉 Tada!"], 
  messagesResult
)
.prefix(4)
.compactMap(\.httpBody)
.compactMap { data in
  try? JSONDecoder()
    .decode(Message.self, from: data)
}
.compactMap { data in
  try? JSONDecoder()
    .decode(Message.self, from: data)
    🟩.message🟥
}
.reduce(into: []) { result, request in
  result.append(request)
}