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
The Past, Present, and Future of Enterprise Java
ivargrimstad
0
360
20260828_品質と開発生産性を両立させる、AI時代のE2Eテストの考え方
magicpod
0
210
数年滞っていたダークモード対応をおよそ2週間で完了させる
chigichan24
0
710
20260914 AIエージェント時代のPlatform Engineering LLM基盤とプロダクトの責務境界線
kanfab1
6
1.9k
更なる可用性を求めて、5年間運用したKotlinのアプリケーションをGoでリプレイスする話
ken_tunc
0
230
Streamlitで実現する自然言語データアプリ開発
ayumu_yamaguchi
0
270
App Storeの外へ──日本のiOSサイドローディング入門 for iOSDC Japan 2026
yuukiw00w
0
210
AWS Step Functions 大規模並列の壁を越える / jaws-sonic-2026-niigata-step-functions
kasacchiful
PRO
1
450
Heart of Swift Concurrency
koher
0
680
Intent as Code
shoppingjaws
6
1k
The Good Stuff, Not the Slop: Engineering High-Quality Android Apps with Modern AI Tooling
danybony
1
250
LoopHub - ローカルで動く GitHub で、AI と共同開発
jugyo
1
550
Featured
See All Featured
Building a A Zero-Code AI SEO Workflow
portentint
PRO
0
720
Organizational Design Perspectives: An Ontology of Organizational Design Elements
kimpetersen
PRO
1
830
Building Experiences: Design Systems, User Experience, and Full Site Editing
marktimemedia
1
610
Un-Boring Meetings
codingconduct
0
420
RailsConf & Balkan Ruby 2019: The Past, Present, and Future of Rails at GitHub
eileencodes
141
35k
Google's AI Overviews - The New Search
badams
0
1.6k
Balancing Empowerment & Direction
lara
6
1.3k
How to Create Impact in a Changing Tech Landscape [PerfNow 2023]
tammyeverts
56
3.5k
エンジニアに許された特別な時間の終わり
watany
109
250k
Bash Introduction
62gerente
615
220k
A designer walks into a library…
pauljervisheath
211
25k
Conquering PDFs: document understanding beyond plain text
inesmontani
PRO
4
3.1k
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?