Skip to content
Rici's Coding Soup. 🍜

Testing with Closures

1 min read

Here's a charade: I have two functions. Can you tell which one below is synchronous and which one is asynchronous?

1func doThis(_ closure: () -> Void) {
2 // ...
3}
4
5func doThat(_ closure: @escaping () -> Void) {
6 // ...
7}

That's a silly question, I know. The answer is - there's no way to know by this code. The implementation is hidden.

But they are slightly different, aren't they? Both accept closures as the argument, but one closure is constrained with the @escaping annotation.

Escaping closures

An escaping closure just means that that block can escape its context, or simply it may not be consumed within the function's body.

The common use for escaping closures is to perform asynchronous operations. In Swift (before the advance of async/await), the way you generally tell the compile to execute a computational intensive task without blocking control is by dispatching it to a background thread and giving it a completion callback for the time it was finished.

But you still can have synchronous tasks running in it. It all depends of your implementation.

This is evident when you're working with Dependency Injection and you have an implementation for you app, but use mocks in your tests. Just because you have a escaping closure, it doesn't mean you need to wait expectations to assert what happens within it.

Example

Below we have an example to demonstrate how this happens.

1// Api Declaration
2protocol Api: AnyClass {
3 func callEndpoing(performTaskWhenApiResponds: @escaping (MyApiResult) -> Void)
4}
5
6// Default Async Api used in the App
7final class DefaultAsyncApi: Api {
8 func callEndpoing(performTaskWhenApiResponds: @escaping (MyApiResult) -> Void) {
9 // Dispatching to another queue, asynchronously
10 DispatchQueue.global(qos: .background).async { [self] in
11 // ... calls network
12 }
13 }
14}
15
16// Our Awesome Api Service
17final class MyAwesomeApiService {
18 private let api: Api
19 init(api: Api) {
20 self.api = api
21 }
22
23 func callEndpoint(_ closure: @escaping (MyApiResult) -> Void) {
24 api.callEndpoing(performTaskWhenApiResponds: closure)
25 }
26}
27
28// ..... TESTING TIME .....
29
30// Mock Api
31final class MockApi: Api {
32 var callEndpointIsCalled = false
33 func callEndpoing(performTaskWhenApiResponds: @escaping (MyApiResult) -> Void) {
34 callEndpointIsCalled = true
35 performTaskWhenApiResponds(/* Api result here */)
36 }
37}
38
39// ... somewhere in (probably) MyAwesomeApiServiceTests.swift
40
41// Testing it with a asynchronous implementation (bad code, don't do that)
42func test_callEndpoint_runsApiCallEndpoint_async() {
43 // Expectation
44 let expects = expectation("Expects callEndpoint(_:) closure to run") // this test NEEDS an expectation, as it runs an asynchronous task. BAD PRACTICE WARNING! Don't use me.
45 // Given
46 let api = DefaultAsyncApi() // using the same object that the app uses
47 let apiService = MyAwesomeApiService(api: api)
48 // When
49 apiService.callEndpoint { result in
50 // Then
51 XCTAssertEqual(result, /* expected result here */) // This line will run asynchronously
52 expects.fulfill()
53 }
54 // Wait
55 wait(for: [expects], time: 0.1)
56}
57
58// Testing it with a synchronous implementation
59func test_callEndpoint_runsApiCallEndpoint_sync() {
60 // Given
61 let mockApi = MockApi() // we have total control over this object
62 let apiService = MyAwesomeApiService(api: mockApi)
63 // When
64 apiService.callEndpoint { result in
65 // Then
66 XCTAssertEqual(result, /* expected result here */) // This line will run synchronously
67 }
68}

MyAwesomeApiService takes an object of type Api and consumes it as a dependency, to forward the call we've asked it to perform to the network. The asynchronous operation lives in the DefaultAsyncApi implementation, but not in MyAwesomeApiService. That means, unless demmanded, callEndpoint(_:) will run synchronously.

That's it for today. Thanks for stopping by once more. See you next time.

© 2022 by Rici's Coding Soup. 🍜. All rights reserved.
Theme by LekoArts