Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Architecting your apps for UI Testing
Search
Karumi
September 15, 2016
Programming
5
650
Architecting your apps for UI Testing
Talk given at NSSpain 2016
Karumi
September 15, 2016
Tweet
Share
More Decks by Karumi
See All by Karumi
A story of comics, neural networks, and Android!
karumi
0
120
Kotlin and Spring boot a pleasant experience and some rough edges
karumi
0
72
kotlin for android developers
karumi
0
160
Dame tus tipos pegaso
karumi
0
110
One page product design
karumi
0
82
Version Control Systems for Researchers
karumi
1
230
Tensor flow for Android
karumi
0
100
Architecture Patterns in Practice with Kotlin
karumi
7
390
Kata Screenshot for Android & iOS
karumi
0
310
Other Decks in Programming
See All in Programming
Amazon S3 NYJavaSIG 2024-12-12
sullis
0
100
アクターシステムに頼らずEvent Sourcingする方法について
j5ik2o
4
290
MCP with Cloudflare Workers
yusukebe
2
220
今年のアップデートで振り返るCDKセキュリティのシフトレフト/2024-cdk-security-shift-left
tomoki10
0
210
Exploring: Partial and Independent Composables
blackbracken
0
100
create_tableをしただけなのに〜囚われのuuid編〜
daisukeshinoku
0
270
創造的活動から切り拓く新たなキャリア 好きから始めてみる夜勤オペレーターからSREへの転身
yjszk
1
130
モバイルアプリにおける自動テストの導入戦略
ostk0069
0
110
KMP와 kotlinx.rpc로 서버와 클라이언트 동기화
kwakeuijin
0
160
テストケースの名前はどうつけるべきか?
orgachem
PRO
0
140
「とりあえず動く」コードはよい、「読みやすい」コードはもっとよい / Code that 'just works' is good, but code that is 'readable' is even better.
mkmk884
3
490
rails statsで大解剖 🔍 “B/43流” のRailsの育て方を歴史とともに振り返ります
shoheimitani
2
940
Featured
See All Featured
Principles of Awesome APIs and How to Build Them.
keavy
126
17k
Designing Experiences People Love
moore
138
23k
The Cult of Friendly URLs
andyhume
78
6.1k
Build your cross-platform service in a week with App Engine
jlugia
229
18k
How GitHub (no longer) Works
holman
311
140k
The Invisible Side of Design
smashingmag
298
50k
Designing Dashboards & Data Visualisations in Web Apps
destraynor
229
52k
Git: the NoSQL Database
bkeepers
PRO
427
64k
Typedesign – Prime Four
hannesfritz
40
2.4k
Statistics for Hackers
jakevdp
796
220k
RailsConf 2023
tenderlove
29
940
Agile that works and the tools we love
rasmusluckow
328
21k
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?