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. 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 URL session for your tests to work with. You'll intercept and record all network requests using a custom URL protocol subclass. In the BlabberTests Utility group, open TestURLProtocol. The minimum protocol requirements are already included in the code. canInit returns true when the current protocol should handle the given URL request. In this case, you always return true since you want to catch all requests. canonicalRequest can alter requests on the fly. In this case, you simply return the given request with no changes. startLoading loads the request and sends a response back to the client, and you call stopLoading when the operation is canceled or when the session should otherwise stop the requests. For these tests, you don't have to add anything here. 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. To get started, add a property to the TestURLProtocol type. Each time TestURLProtocol responds to a request, you'll store it in lastRequest, so you can verify its contents. This property is static. Because of the way you pass these URL protocols to your URL session configuration, you can't easily access instant properties. For the simple tests in this course, this works fine. Next, add some code at the bottom of startLoading. First, check that the request has a non-nil httpBodyStream input stream. This is the stream you use to read the request data. Next, make a new mutable request variable so you can modify the request before storing it. Then read the request contents from httpBodyStream and store the data in httpBody. Finally, save the request in lastRequest. Now your tests can verify the contents after the network call completes. You're all set to use TestURLProtocol to test BlabberModel. In BlabberTests, create a model property, with a closure to initialize it. First, create a new BlabberModel with username "test". Then create a URLSessionConfiguration that uses TestURLProtocol. And tell the model to use this new session. And return the model. TestURLProtocol will handle all the network calls made by this instance of BlabberModel, so you can inspect them in your tests. And finally, write your first test. When creating asynchronous tests, remember to add the async keyword to each test method. Doing this lets you await your code under test and easily verify the output. Model is already configured to use the test suitable urlSession. So you don't need to do any additional setup, just call model.say right away. Next, add a test expectation. First, unwrap the optional test protocol.lastRequest. Then check the URL matches the expected address. You verify that the lastRequest the network performed, this model.say "hello", was sent to the correct URL. If the model sends the data to the correct endpoint, check that it also sends the correct data. First, unwrap the request body. Then decode the request body, it should decode as a message. And the decoded message should be "Hello!". If you've written asynchronous test before, you'll appreciate how short and clear this test code is. If you haven't written asynchronous test before, you really don't need to know how much effort you used to need to set up a good asynchronous test. Now check the simulator device is one that's already started up. And run the test. Click play in the editor gutter, to the left of func testModelSay, or press Command + U to run all tests. Success, that was easy. Now, how to test asynchronous work that may yield many values like countdown. This sequence requires up to four network requests. To guarantee the method works correctly, you must verify more than the last value. Add some properties to TestURLProtocol. Add a static property holding a continuation. Add a static property that returns an asynchronous stream that emits requests. Store the AsyncStream's Continuation so you can emit a value each time TestURLProtocol responds to a request. Continuation is a static property, so you can have only one active instance of requests at a time. To emit a value, add a didSet handler to lastRequest. Now updating lastRequest will also emit the request as an element of the asynchronous stream that requests returns. Back in BlabberTests, add another test. First call countdown. And iterate over the stream of requests to print the recorded values. It hangs, stop execution, then set break points on all three lines, and run the test again. The debugger stops at the first break point. Click continue program execution. It stops at the second break point, click continue program execution. But you never reach the print statement. By the way, notice that await doesn't time out. You'll fix that in a later episode. Stop execution and disable the break points. So what's the problem here? In TestURLProtocol, look at how you emit the requests. You only emit values when lastRequest is set. Back in BlabberTests, by the time this for await loop starts, countdown has already finished, so there aren't any requests to read. You need both tasks to run at the same time, and you remember how to use async let to do this. In TestModelCountdown, instead of try await model.countdown, give the call a name so you can await it later. Because countdown doesn't return to value, you specify its binding type as void to avoid a warning. Also give a name to requests. And delete the for await closure. The closure was just to print the requests to demonstrate it was hanging and now wait for them. You only care about the messages, then check the results. The error message is because you need to process requests to get it to look like this. Add this modifier to TestURLProtocol.requests. You only need as many requests as you expect during a successful run of countdown, the four requests that produce these four messages. You need to extract the messages from the requests. Start with the httpBody of each request, then decode the body as a message. And return its message property. Finally, collect these messages into an array. reduce runs this closure for each element in the sequence and adds each request to result. And now you can process the elements as a simple plain array, run the test. After the countdown finishes, your test succeeds, great work. But there are still a couple of issues. The execution time is more than five seconds because the app waits one second for each request so your test has to wait too. It would be good to be able to speed up execution for your tests. Also, the code will hang if you only get three requests instead of the expected four. The execution will stop at prefix 4 and wait for a fourth element. Test this by asking for five results. You really need some kind of timeout mechanism. Change back to four results and stop execution. You'll take care of these issues after the next two episodes where you'll learn about manual continuations.