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
430
Kata Screenshot for Android & iOS
karumi
0
380
Other Decks in Programming
See All in Programming
App Storeの外へ──日本のiOSサイドローディング入門 for iOSDC Japan 2026
yuukiw00w
0
200
Go のフラットな パッケージに 「ファイル単位の private」を 〜Linter “declscope” を作った話〜
mpyw
0
250
モバイル交通系ICへのチャージ実例から考える、クロスプラットフォーム開発におけるiOS実機テスト設計とCI運用
yusuga
1
450
What We Talk About When We Talk About XP
m_seki
2
610
Streamlitで実現する自然言語データアプリ開発
ayumu_yamaguchi
0
250
Building an Out-of-Order CPU
latte72
1
770
The Good Stuff, Not the Slop: Engineering High-Quality Android Apps with Modern AI Tooling
danybony
1
250
20260828_品質と開発生産性を両立させる、AI時代のE2Eテストの考え方
magicpod
0
200
Webの地図
yosuke_furukawa
PRO
6
4.4k
選挙速報を多くのユーザーへ 届ける Live Activities 設計
hamayokokuririn
0
130
AI × TiDD / 2026.09.05 Redmine 大阪
tokudiro
1
150
AGENTS.md Is Not Enough:Build Skills, Don't Download Them
lx_t
0
110
Featured
See All Featured
How STYLIGHT went responsive
nonsquared
100
6.3k
What the history of the web can teach us about the future of AI
inesmontani
PRO
1
690
Lightning talk: Run Django tests with GitHub Actions
sabderemane
0
250
Mind Mapping
helmedeiros
1
360
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
230
23k
世界の人気アプリ100個を分析して見えたペイウォール設計の心得
akihiro_kokubo
PRO
74
42k
Max Prin - Stacking Signals: How International SEO Comes Together (And Falls Apart)
techseoconnect
PRO
0
460
Navigating Algorithm Shifts & AI Overviews - #SMXNext
aleyda
1
1.6k
Neural Spatial Audio Processing for Sound Field Analysis and Control
skoyamalab
0
500
The State of eCommerce SEO: How to Win in Today's Products SERPs - #SEOweek
aleyda
2
12k
A designer walks into a library…
pauljervisheath
211
25k
How to build a perfect <img>
jonoalderson
1
6k
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?