Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Architecting your apps for UI Testing
Search
Karumi
September 15, 2016
Programming
720
5
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Architecting your apps for UI Testing
Talk given at NSSpain 2016
Karumi
September 15, 2016
More Decks by Karumi
See All by Karumi
A story of comics, neural networks, and Android!
karumi
0
190
Kotlin and Spring boot a pleasant experience and some rough edges
karumi
0
110
kotlin for android developers
karumi
0
230
Dame tus tipos pegaso
karumi
0
170
One page product design
karumi
0
150
Version Control Systems for Researchers
karumi
1
300
Tensor flow for Android
karumi
0
160
Architecture Patterns in Practice with Kotlin
karumi
7
440
Kata Screenshot for Android & iOS
karumi
0
380
Other Decks in Programming
See All in Programming
更なる可用性を求めて、5年間運用したKotlinのアプリケーションをGoでリプレイスする話
ken_tunc
0
380
AHC070解法紹介
eijirou
0
140
ゲームコントローラやキーボードのファームウェアをSwiftで書く
kishikawakatsumi
1
260
手動確認はもう限界 〜XCUITestでCustom URL Schemeの遷移を起動種別ごとに自動テストする〜 / Testing Custom URL Schemes with XCUITest
otouto
0
350
UPDATE をやめる — EF Core でマスタをバージョン管理する
panda728
PRO
0
850
MVNOの申込からeSIM開通までをiOSアプリでつなぐ- 本人確認・MNP・通信事業者基盤をまたぐ実装
satotakeshi
0
490
AI が書く Go コードの品質を劇的に向上させる Linter: “declscope”
mpyw
0
440
アクセシビリティから考える情報設計
high_g_engineer
0
420
標準パッケージに uuid が追加された 背景から見る Go らしい意思決定 / go_127_uuid_decision
convto
5
8.8k
Are APIs Still Relevant in the AI Era?
soyuka
0
330
選挙速報を多くのユーザーへ 届ける Live Activities 設計
hamayokokuririn
0
180
[2026-09-26]空論ジェネリックプロセス~テスト資産とAIで紡ぐ、再現可能なパフォーマンスチューニングの話~
tosite
0
200
Featured
See All Featured
The AI Search Optimization Roadmap by Aleyda Solis
aleyda
1
6.2k
Applied NLP in the Age of Generative AI
inesmontani
PRO
4
2.5k
DevOps and Value Stream Thinking: Enabling flow, efficiency and business value
helenjbeal
1
390
AI Search: Where Are We & What Can We Do About It?
aleyda
0
8k
Design and Strategy: How to Deal with People Who Don’t "Get" Design
morganepeng
133
19k
Marketing Yourself as an Engineer | Alaka | Gurzu
gurzu
0
310
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
333
23k
SEO in 2025: How to Prepare for the Future of Search
ipullrank
3
3.8k
Navigating Team Friction
lara
192
16k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
194
17k
Evolving SEO for Evolving Search Engines
ryanjones
0
300
Highjacked: Video Game Concept Design
rkendrick25
PRO
1
460
Transcript
Architecting your apps for UI Testing Alberto Gragera Co-Founder and
Technical Director @gragera_
[email protected]
This is NOT about acceptance tests
Why invest in UI Testing?
Test from the user’s point of view
Reproduce any imaginable scenario
Architecture
GOOD ARCHITECTURE Readability Testability Embrace change Robustness
BAD ARCHITECTURE Fear of change Lots of WTFs Hacks Mystic
knowledge
What main features should your architecture have to enable you
to write UI tests effectively?
Each layer must have a crystal clear purpose and responsibilities
Be able to replace the dependencies to inject test doubles
Disclaimer: Viper-ish
UI PRESENTER USE CASE NAVIGATOR REPOSITORY DATA SOURCE … N
… COMPOSITION ROOT COMPOSITION ROOT A … N …
UI PRESENTER USE CASE NAVIGATOR REPOSITORY DATA SOURCE … N
COMPOSITION ROOT COMPOSITION ROOT A … N … …
UI Abstraction over UIViewController Passive Views
Use Cases Gateway to the “data” world Expose what your
domain layer can do
Navigator Navigates from one screen to another Knows about the
state of the screen at any given time Handle the presentation of this new screen
CompositionRoot Knows how to instantiate every collaborator of the app
Knows about the particulars of a collaborator
Presenter Tell the UI what to do Tell Navigators where
the user wants to go Execute Use Cases KEY point for dependency injection
How does this look like in a real app?
FoursquareTop
App launch
Screen AppDelegate private func installRootNavigator() { let builder = AppCompositionRoot.Builder()
appCompositionRoot = builder.with( venueCompositionRoot: VenueCompositionRoot() ).build() navigator = RootNavigator( appCompositionRoot: appCompositionRoot ) navigator.installRootViewController(window!) window?.makeKeyAndVisible() } 1 2 3
DEPENDENCIES RootNavigator func installRootViewController(window: UIWindow) { let vc = appCompositionRoot.getInitialViewController()
currentNavigationController = vc window.rootViewController = vc }
DEPENDENCIES AppCompositionRoot class AppCompositionRoot { let venue: VenueCompositionRoot func getInitialViewController()
-> UINavigationController { let vc = venue.getBestVenuesAroundViewController() return UINavigationController(rootViewController: vc) } }
DEPENDENCIES VenueCompositionRoot func getBestVenuesAroundViewController() -> UIViewController { let vc =
BestVenuesAroundYouViewController() vc.venuesAroundYouPresenter = BestVenuesAroundYouPresenter( ui: vc, getBestPlacesAroundYouUseCase: getPlacesAroundYouUseCase(), getUserLocationUseCase: getUserLocationUseCase(), navigator: getVenueListNavigator() ) return vc }
Going to a venue detail
TAP
presenter.venueSelected(venue) UI navigator.goTo(venue) PRESENTER compositionRoot.getVC(venue) pushViewController() NAVIGATOR
Screen BestVenuesAroundYouViewController func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { guard
let venue = ds.venue(atIndexPath: indexPath) else { return } venuesAroundYouPresenter.venueSelected(venue) }
Screen BestVenuesAroundYouPresenter func venueSelected(venue: VenueViewModel) { navigator.goTo(venueDetail: venue) }
DEPENDENCIES VenueListNavigator func goTo(venueDetail venue: VenueViewModel) { let vc =
appCompositionRoot.venue.getVenueDetailViewController( forVenue: venue ) currentNavigationController?.pushViewController( vc, animated: true ) }
Testing this
STRATEGY
UI PRESENTER USE CASE NAVIGATOR REPOSITORY DATA SOURCE … N
… COMPOSITION ROOT COMPOSITION ROOT A … N …
TEST main.swift let appDelegateClass = Environment().isInTestingRun() ? NSStringFromClass(AppDelegateForTests) : NSStringFromClass(AppDelegate)
UIApplicationMain( Process.argc, Process.unsafeArgv, nil, appDelegateClass )
TEST AppDelegateForTests func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?)
-> Bool { window = UIWindow(frame: UIScreen.mainScreen().bounds )! window.backgroundColor = .whiteColor() window.makeKeyAndVisible() window.rootViewController = UINavigationController() return true }
TEST BaseUITest func openViewController() { let appCompositionRoot = getTestingAppCompositionRoot( baseAppCompositionRoot
) rootNavigator = RootNavigator( appCompositionRoot: appCompositionRoot ) presentViewController( keyWindow, navigator: rootNavigator, appCompositionRoot: appCompositionRoot ) } 1 2 3
Test Cases
TEST BestVenuesAroundYouViewControllerTests class BestVenuesAroundYouViewControllerTests: BaseUITestCase { private var stubVenueListNavigator: StubVenueListNavigator!
private var getUserLocation: StubGetUserLocationUseCase! private var getBestPlacesAroundYou: StubGetBestPlacesAroundYouUseCase! override func setUp() { super.setUp() getUserLocation = StubGetUserLocationUseCase() getBestPlacesAroundYou = StubGetBestPlacesAroundYouUseCase() stubVenueListNavigator = StubVenueListNavigator() }
TEST BestVenuesAroundYouViewControllerTests override func getTestingAppCompositionRoot(compositionRoot: AppCompositionRoot) -> AppCompositionRoot { return
AppCompositionRoot.Builder() .with(venueCompositionRoot: getVenueCompositionRoot()) .build() } private func getVenueCompositionRoot() -> VenueCompositionRoot { let venueCompositionRoot = TestingVenueCompositionRoot() venueCompositionRoot.stubGetUserLocation = getUserLocation venueCompositionRoot.stubGetBestPlacesAroundYou = getBestPlacesAroundYou venueCompositionRoot.stubVenueListNavigator = stubVenueListNavigator return venueCompositionRoot }
TEST func getGetUserLocationUseCase() -> GetUserLocationUseCase { return GetUserLocation( gps: getGPS(),
repository: getVenueDataSource() ) } func getGetUserLocationUseCase() -> GetUserLocationUseCase { return stubGetUserLocationUseCase } VenueCompositionRoot TestingVenueCompositionRoot
TEST BestVenuesAroundYouViewControllerTests func testLoadingIndicatorIsVisibleWhenLoadingVenues() { givenWeAreFetchingUsersLocation() openViewController() waitForViewWithLocalizedAccessibilityLabel(.Loading) } override
func presentViewController(window: UIWindow, navigator: RootNavigator, appCompositionRoot: AppCompositionRoot) { presentViewController( appCompositionRoot.venue.getBestVenuesAroundViewController() ) }
TEST BestVenuesAroundYouViewControllerTests func testOpenSettingsIfTheGPSAccessIsDeniedAndTheUserOpensSettings() { givenThereWillBeAnErrorFetchingUserLocation(error: .AccessDenied) openViewController() tapGoToSettings() expect(
stubVenueListNavigator.didNavigateToSettings ).toEventually( equal(true) ) } GIVEN WHEN THEN
TEST BestVenuesAroundYouViewControllerTests func testRetryShowsVenuesAfterThereIsAnErrorFetchingUsersLocation() { getUserLocationUseCase.givenThereWillBeAnErrorFetchingUsersLocation() openViewController() expectCanNotFetchLocationError(toBeVisible: true) getBestPlacesAroundYouUseCase.givenThereWillBeVenues()
tapRetry() expectVenueList( getBestPlacesAroundYouUseCase.venueList ) expectCanNotFetchLocationError(toBeVisible: false) }
Testing the real navigation
TEST BestVenuesAroundYouViewControllerNavigationTests override func presentViewController(window: UIWindow, navigator: RootNavigator, appCompositionRoot: AppCompositionRoot)
{ navigator.installRootViewController(window) } override func getTestingAppCompositionRoot(compositionRoot: AppCompositionRoot) -> AppCompositionRoot { return AppCompositionRoot.Builder(compositionRoot: compositionRoot) .with(venueCompositionRoot: getVenueCompositionRoot()) .build() } private func getVenueCompositionRoot() -> VenueCompositionRoot { let venueCompositionRoot = TestingVenueCompositionRoot() venueCompositionRoot.stubGetUserLocationUseCase = getUserLocationUseCase venueCompositionRoot.stubGetBestPlacesAroundYouUseCase = getBestPlacesAroundYouUseCase venueCompositionRoot.stubGetVenueDetailsUseCase = getVenueDetailsUseCase return venueCompositionRoot } private func getVenueCompositionRoot() -> VenueCompositionRoot { let venueCompositionRoot = TestingVenueCompositionRoot() venueCompositionRoot.stubGetUserLocation = getUserLocation venueCompositionRoot.stubGetBestPlacesAroundYou = getBestPlacesAroundYou venueCompositionRoot.stubVenueListNavigator = stubVenueListNavigator return venueCompositionRoot } BestVenuesAroundYouViewControllerTests
TEST BestVenuesAroundYouViewControllerNavigationTests func testNavigatesToVenueDetailWhenUserTapsOnVenue() { getVenuesUseCase.givenThereWillBeVenues() let topVenue = getVenuesUseCase.getTopVenue()
getVenueDetailsUseCase.venue = topVenue openViewController() tapTopVenue() expectTitle(ofVenue: topVenue, toBeVisible: true) } GIVEN WHEN THEN
Why is all this possible?
Thanks!
Questions?