-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathblockbased-ui-testing-in-swift.html
425 lines (373 loc) · 21.8 KB
/
blockbased-ui-testing-in-swift.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://use.fontawesome.com/afd448ce82.js"></script>
<!-- Meta Tag -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- SEO -->
<meta name="author" content="Bruno Rocha">
<meta name="keywords" content="Software, Engineering, Blog, Posts, iOS, Xcode, Swift, Articles, Tutorials, OBJ-C, Objective-C, Apple">
<meta name="description" content="When deadlines are tight and the product faces considerable changes, it's common for developers to make concessions in the project's quality to make sure it gets shipped in time. This leads to release anxiety - that stressful feeling where you're unsure if you're shipping something that actually works.">
<meta name="title" content="Avoiding Release Anxiety 1: Block-based UI Testing in Swift">
<meta name="url" content="https://swiftrocks.com/blockbased-ui-testing-in-swift">
<meta name="image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4">
<meta name="copyright" content="Bruno Rocha">
<meta name="robots" content="index,follow">
<meta property="og:title" content="Avoiding Release Anxiety 1: Block-based UI Testing in Swift"/>
<meta property="og:image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4"/>
<meta property="og:description" content="When deadlines are tight and the product faces considerable changes, it's common for developers to make concessions in the project's quality to make sure it gets shipped in time. This leads to release anxiety - that stressful feeling where you're unsure if you're shipping something that actually works."/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="https://swiftrocks.com/blockbased-ui-testing-in-swift"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4"/>
<meta name="twitter:image:alt" content="Page Thumbnail"/>
<meta name="twitter:title" content="Avoiding Release Anxiety 1: Block-based UI Testing in Swift"/>
<meta name="twitter:description" content="When deadlines are tight and the product faces considerable changes, it's common for developers to make concessions in the project's quality to make sure it gets shipped in time. This leads to release anxiety - that stressful feeling where you're unsure if you're shipping something that actually works."/>
<meta name="twitter:site" content="@rockbruno_"/>
<!-- Favicon -->
<link rel="icon" type="image/png" href="images/favicon/iconsmall2.png" sizes="32x32" />
<link rel="apple-touch-icon" href="images/favicon/iconsmall2.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet">
<link rel="canonical" href="https://swiftrocks.com/blockbased-ui-testing-in-swift"/>
<!-- Bootstrap CSS Plugins -->
<link rel="stylesheet" type="text/css" href="css/bootstrap.css">
<!-- Prism CSS Stylesheet -->
<link rel="stylesheet" type="text/css" href="css/prism4.css">
<!-- Main CSS Stylesheet -->
<link rel="stylesheet" type="text/css" href="css/style48.css">
<link rel="stylesheet" type="text/css" href="css/sponsor4.css">
<!-- HTML5 shiv and Respond.js support IE8 or Older for HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://swiftrocks.com/blockbased-ui-testing-in-swift"
},
"image": [
"https://swiftrocks.com/images/thumbs/thumb.jpg"
],
"datePublished": "2019-04-20T18:00:00+00:00",
"dateModified": "2020-04-12T14:00:00+02:00",
"author": {
"@type": "Person",
"name": "Bruno Rocha"
},
"publisher": {
"@type": "Organization",
"name": "SwiftRocks",
"logo": {
"@type": "ImageObject",
"url": "https://swiftrocks.com/images/thumbs/thumb.jpg"
}
},
"headline": "Avoiding Release Anxiety 1: Block-based UI Testing in Swift",
"abstract": "When deadlines are tight and the product faces considerable changes, it's common for developers to make concessions in the project's quality to make sure it gets shipped in time. This leads to release anxiety - that stressful feeling where you're unsure if you're shipping something that actually works."
}
</script>
</head>
<body>
<div id="main">
<!-- Blog Header -->
<!-- Blog Post (Right Sidebar) Start -->
<div class="container">
<div class="col-xs-12">
<div class="page-body">
<div class="row">
<div><a href="https://swiftrocks.com">
<img id="logo" class="logo" alt="SwiftRocks" src="images/bg/logo2light.png">
</a>
<div class="menu-large">
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-large">
<div class="menu-item">
<a href="blog">blog</a>
</div>
<div class="menu-item">
<a href="about">about</a>
</div>
<div class="menu-item">
<a href="talks">talks</a>
</div>
<div class="menu-item">
<a href="projects">projects</a>
</div>
<div class="menu-item">
<a href="software-engineering-book-recommendations">book recs</a>
</div>
<div class="menu-item">
<a href="games">game recs</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
</div>
<div class="menu-small">
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-small-1">
<div class="menu-item">
<a href="blog">blog</a>
</div>
<div class="menu-item">
<a href="about">about</a>
</div>
<div class="menu-item">
<a href="talks">talks</a>
</div>
<div class="menu-item">
<a href="projects">projects</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-small-2">
<div class="menu-item">
<a href="software-engineering-book-recommendations">book recs</a>
</div>
<div class="menu-item">
<a href="games">game recs</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
</div>
</div>
<div class="content-page" id="WRITEIT_DYNAMIC_CONTENT">
<!--WRITEIT_POST_NAME=Avoiding Release Anxiety 1: Block-based UI Testing in Swift-->
<!--WRITEIT_POST_HTML_NAME=blockbased-ui-testing-in-swift-->
<!--WRITEIT_POST_SITEMAP_DATE_LAST_MOD=2020-04-12T14:00:00+02:00-->
<!--WRITEIT_POST_SITEMAP_DATE=2019-04-20T18:00:00+00:00-->
<!--Add here the additional properties that you want each page to possess.-->
<!--These properties can be used to change content in the template page or in the page itself as shown here.-->
<!--Properties must start with 'WRITE_IT_POST'.-->
<!--Writeit provides and injects WRITEIT_POST_NAME and WRITEIT_POST_HTML_NAME by default.-->
<!--WRITEIT_POST_SHORT_DESCRIPTION=When deadlines are tight and the product faces considerable changes, it's common for developers to make concessions in the project's quality to make sure it gets shipped in time. This leads to release anxiety - that stressful feeling where you're unsure if you're shipping something that actually works.-->
<title>Avoiding Release Anxiety 1: Block-based UI Testing in Swift</title>
<div class="blog-post">
<div class="post-title-index">
<h1>Avoiding Release Anxiety 1: Block-based UI Testing in Swift</h1>
</div>
<div class="post-info">
<div class="post-info-text">
Published on 20 Apr 2019
</div>
</div>
<p>When deadlines are tight and the product faces considerable changes, it's common for developers to make concessions in the project's quality to make sure it gets shipped in time. This leads to release anxiety - that stressful feeling where you're unsure if you're shipping something that actually works. As a result, teams resort to heavy amounts of manual testing, staying overtime to make sure nothing fell apart and an unhappy environment in general. <i>Avoiding Release Anxiety</i> is a series of posts where I show the things I do to develop durable Swift apps that allow me and my team to sleep worry-free at night.</p>
<h2>Resilient UI Tests</h2>
<div class="sponsor-article-ad-auto hidden"></div>
<p>I feel that UI Tests are very underrated by the community when compared to other forms of testing, but they're my favorite. When the subject is making sure that a feature works, my opinion is that users don't care if a button isn't the right color or if a font is slightly off -- they only care if it does what it should do. UI Testing is the closest you can get to the actual user experience, so if the UI Tests are working, it's likely that the user's experience in these flows will work as well. As a complement to the usual unit tests, I make sure to always write UI tests that navigate through all variations of the important flows of the app to reduce stress in release days.</p>
<p>With that said, how can you effectively write and maintain these tests if your app has several screens and variations of them?</p>
<p>I deal with this by implementing something that I call block-based testing. Instead of putting all the test logic directly in the test method, I instead divide each step of the navigation into its own "exploration" method. Here's an example:</p>
<pre><code>private func exploreBalanceDetailsScreen() {
let balanceView = app.tables.buttons["WALLET_HEADER"]
expect(balanceView.isHittable) == true
balanceView.tap()
let detailsView = app.otherElements["BALANCE_DETAILS_VC_ROOT_VIEW"]
detailsView.waitForExistence("Expected to be in the Balance Details Screen!")
//Conditions that test if "Balance Details" is behaving correctly
app.tapNavigationBackButton()
}</code></pre>
<p>Exploration methods start by moving to the relevant screen of the method (assuming that the app is already in a position to do so). After it's confirmed that the relevant screen is working, the app is moved back to where it was before the exploration started.</p>
<p>To confirm that a screen change happened, I like to use this <code>waitForExistence()</code> helper in a view controller's root <code>view</code> property:</p>
<pre><code>extension XCUIElement {
func waitForExistence(_ description: String? = nil, timeout: TimeInterval = 0.2) {
let predicate = NSPredicate(format: "exists == true")
let hasAppeared = XCTNSPredicateExpectation(predicate: predicate,
object: self)
_ = XCTWaiter.wait(for: [hasAppeared], timeout: timeout)
}
}</code></pre>
<p>When all relevant flows are created in this structure, building test cases is just a matter of connecting your LEGO bricks:</p>
<pre><code>class BaseUITestCase: XCTestCase {
private(set) var app: XCUIApplication! = nil
override func setUp() {
super.setUp()
continueAfterFailure = false
if app == nil {
app = XCUIApplication()
app.add(MockFlags.Environment.isUITest)
}
}
}
class WalletViewUITests: BaseUITestCase {
func testLoggedOnHomeToQRScannerFlow() {
app.launch()
expectToBeInHome()
exploreAvailableCardsWidget()
exploreBalanceDetailsScreen()
explorePaymentDetailsScreen()
exploreTransactionsListScreen()
goToQRScanner()
}
}</code></pre>
<p>By reorganizing these blocks, you can test several variations of a flow with little effort. Here's an example of how I test the logged out version of the previous test:</p>
<pre><code>func testLoggedOffHomeToQRScannerFlow() {
app.add(MockFlags.User.isNotLogged)
app.launch()
expectToBeInHome(canInteract: false) //There should be a "logged off" empty state covering the screen.
exploreLoggedOffScreen(shouldLogin: true)
testLoggedOnHomeToQRScannerFlow() //Being logged out initially should not affect the rest of the app.
}</code></pre>
<p>One of the things that bothers me the most in programming is when the tests are so complicated that they end up becoming a burden. Developing tests with a defined structure like this makes them a lot easier to maintain and expand from.</p>
<h2>Extra: Typesafe Mocks</h2>
<p>Although the block structure helps with the development of the tests themselves, we still need to make sure the app can properly navigate the screens and understand when an alternate flow is required.</p>
<p>As spoiled by the previous example, the way we chose to handle the latter is by using <code>MockFlags</code>, which is a simple enum created to represent launch arguments:</p>
<pre><code>public protocol MockFlag {
var value: String { get }
}
extension MockFlag {
public var value: String {
return "\(String(describing: type(of: self)))-\(String(describing: self))"
}
}
public enum MockFlags {
public enum Environment: MockFlag {
case isUITest
case isUnitTest
}
public enum User: MockFlag {
case isNotLogged
}
}
extension XCUIApplication {
func add(_ mockFlag: MockFlag) {
launchArguments.append(mockFlag.value)
}
}
public func has(_ mockFlag: MockFlag) -> Bool {
return CommandLine.arguments.contains(mockFlag.value)
}</code></pre>
<p>By running <code>app.add(theFlagIWantToTest)</code> before launching the UI test, the desired flag becomes available for inspection during runtime as a launch argument. The tough part is how you efficiently react to these flags, but developing a system that can do this well is very beneficial to the evolution of your project.</p>
<p>The way I like to do this is by using <b>protocols</b> in all important components of the app. Every one of these components, such as clients and persistence modules, are initialized once in the app's launch and propagated through the app -- but when the app is in the Unit/UI test environment, different, mocked versions of them are created instead. This is the usual stuff, but the catch here is that since everything is done with protocols, the app itself is completely isolated from the environment peculiarities. This prevents the tests from making the project's maintenance harder.</p>
<p>Here's a basic example of how a client is mocked in this structure:</p>
<pre><code>protocol HTTPRequest {
associatedtype Response //Omitting paths, parameters, headers and etc for simplicity
}
protocol HTTPClient: AnyObject {
func send<R>(_ request: R) -> Promise<R.Response> where R: HTTPRequest
}
final class MockClient: HTTPClient {
func send<R>(_ request: R) -> Promise<R.Response> where R: HTTPRequest {
guard let value = (request as? Mockable)?.mockedValue else {
Logger.log("Request \(request) has no mocked version!")
return Promise(error: HTTPError.generic)
}
return Promise(value: value)
}
}</code></pre>
<p>To make the mocks typesafe, we created a <code>Mockable</code> protocol that allows anything to expose a mocked version of itself based on the available <code>MockFlags</code>. This way, we don't have to deal with HTTP stub libraries and raw json strings that need to be replaced every now and then, and changes in the app's models will make so the mocks have to be updated in compile time as well. The mocked client ends up being simply a class that retrieves these <code>Mockable</code> <code>HTTPRequest's</code> properties.</p>
<pre><code>protocol Mockable {
associatedtype MockValue
var mockedValue: MockValue { get }
}
extension HTTPRequest where Self: Mockable {
typealias MockValue = Response
}</code></pre>
<pre><code>extension UserBalanceRequest: Mockable {
var mockedValue: UserBalanceResponse {
return UserBalanceResponse(balance: has(MockFlag.User.noBalance) ? 0 : 1_000_000)
}
}</code></pre>
<p>To start the logic that propagates the mocked components forward, I like to use the <b>factory</b> pattern to hide the decision that results in their creation. In the case of the client, we allow the <code>AppDelegate</code> to create and retain these components before injecting them to the app's first screen.</p>
<pre><code>func isTestEnvironment() -> Bool {
return has(MockFlags.Environment.isUITest) || has(MockFlags.Environment.isUnitTest)
}</code></pre>
<pre><code>enum HTTPClientFactory {
static func create() -> HTTPClient {
if isTestEnvironment() {
return MockClient()
}
return URLSessionHTTPClient() //The regular HTTPClient
}
}</code></pre>
<pre><code>class AppDelegate: UIResponder, UIApplicationDelegate {
let client = HTTPClientFactory.create()
...</code></pre>
<p>Although creating the components is easy enough, how you efficiently push them forward to the rest of the app depends on your architecture. We use MVVM-C, so in our case we chose to never use singletons to instead have each Coordinator/ViewModel directly receive and retain the components that are important to them.</p>
<pre><code>final class WalletCoordinator: Coordinator {
let client: HTTPClient
let persistence: Persistence
init(client: HTTPClient, persistence: Persistence) {
self.client = client
self.persistence = persistence
let viewModel = WalletViewModel(client: client, persistence: persistence)
let viewController = WalletViewController(viewModel: viewModel)
super.init(rootViewController: viewController, delegate: delegate)
}
}</code></pre>
<p>This is beneficial to the development of Unit/UI tests as testing certain interactions can be done by simply passing custom versions of the components to each class.</p>
<pre><code>func testViewStates() {
let mockedClient = MockClient()
mockedClient.alwaysFail = true
let viewModel = WalletViewModel(client: mockedClient, persistence: MockPersistence())
expect(viewModel.state) == .none
viewModel.load()
expect(viewModel.state) == .failed
}</code></pre>
<pre><code>func testLockId() {
class FailableClient: MockClient {
var shouldFail = false
func send<R>(_ resource: R) -> Promise<R.Value> where R : HTTPRequest {
guard shouldFail else {
return super.send(resource)
}
return Promise(error: HTTPError.generic)
}
}
let client = FailableClient()
let viewModel = CheckoutViewModel(client: client)
// some tests
client.shouldFail = true
// more tests
}</code></pre>
<p>The downside of this approach is that the initializers tend to get really big when a screen has tons of responsibilities, which can be a bit jarring. There are several alternatives to dependency injection out there that don't hurt the testability of the classes, but that's a topic for another blog post.</p>
<h2>Conclusion</h2>
<div class="sponsor-article-ad-auto hidden"></div>
<p>With everything setup, we're able to create not just UI but also Unit Tests while still being able to scale our project stress-free. By separating the test necessities from the project itself, we can develop conditions for tests without hurting the project's quality. After creating exploration methods and developing UI Tests for important flows that snap these methods together, we can rely on a resilient test suite that allows us to safely release new versions of our apps.</p>
<p>Follow me on my Twitter - <a href="https://twitter.com/rockbruno_">@rockbruno_</a>, and let me know of any suggestions and corrections you want to share.</p>
</div></div>
<div class="blog-post footer-main">
<div class="footer-logos">
<a href="https://swiftrocks.com/rss.xml"><i class="fa fa-rss"></i></a>
<a href="https://twitter.com/rockbruno_"><i class="fa fa-twitter"></i></a>
<a href="https://github.com/rockbruno"><i class="fa fa-github"></i></a>
</div>
<div class="footer-text">
© 2025 Bruno Rocha
</div>
<div class="footer-text">
<p><a href="https://swiftrocks.com">Home</a> / <a href="blog">See all posts</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Blog Post (Right Sidebar) End -->
</div>
</div>
</div>
<!-- All Javascript Plugins -->
<script type="text/javascript" src="js/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script type="text/javascript" src="js/prism4.js"></script>
<!-- Main Javascript File -->
<script type="text/javascript" src="js/scripts30.js"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-H8KZTWSQ1R"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-H8KZTWSQ1R');
</script>
</body>
</html>