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
640
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
68
kotlin for android developers
karumi
0
150
Dame tus tipos pegaso
karumi
0
100
One page product design
karumi
0
81
Version Control Systems for Researchers
karumi
1
220
Tensor flow for Android
karumi
0
99
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
Streams APIとTCPフロー制御 / Web Streams API and TCP flow control
tasshi
1
300
2万ページのSSG運用における工夫と注意点 / Vue Fes Japan 2024
chinen
3
1.4k
デプロイを任されたので、教わった通りにデプロイしたら障害になった件 ~俺のやらかしを越えてゆけ~
techouse
52
32k
Jakarta Concurrencyによる並行処理プログラミングの始め方 (JJUG CCC 2024 Fall)
tnagao7
1
230
生成 AI を活用した toitta 切片分類機能の裏側 / Inside toitta's AI-Based Factoid Clustering
pokutuna
0
580
役立つログに取り組もう
irof
27
8.7k
Modern Angular: Renovation for Your Applications
manfredsteyer
PRO
0
210
詳細解説! ArrayListの仕組みと実装
yujisoftware
0
480
OpenTelemetryでRailsのパフォーマンス分析を始めてみよう(KoR2024)
ymtdzzz
4
1.6k
Identifying User Idenity
moro
6
7.9k
CSC305 Lecture 13
javiergs
PRO
0
130
外部システム連携先が10を超えるシステムでのアーキテクチャ設計・実装事例
kiwasaki
1
230
Featured
See All Featured
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
41
2.1k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
3
370
Designing for Performance
lara
604
68k
The Cult of Friendly URLs
andyhume
78
6k
Fireside Chat
paigeccino
32
3k
Testing 201, or: Great Expectations
jmmastey
38
7k
How STYLIGHT went responsive
nonsquared
95
5.2k
What's new in Ruby 2.0
geeforr
342
31k
Building Your Own Lightsaber
phodgson
102
6.1k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
PRO
9
680
Faster Mobile Websites
deanohume
304
30k
Fantastic passwords and where to find them - at NoRuKo
philnash
50
2.8k
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?