Slide 1

Slide 1 text

Architecting your apps for UI Testing Alberto Gragera Co-Founder and Technical Director @gragera_ [email protected]

Slide 2

Slide 2 text

This is NOT about acceptance tests

Slide 3

Slide 3 text

Why invest in UI Testing?

Slide 4

Slide 4 text

Test from the user’s point of view

Slide 5

Slide 5 text

Reproduce any imaginable scenario

Slide 6

Slide 6 text

Architecture

Slide 7

Slide 7 text

GOOD ARCHITECTURE Readability Testability Embrace change Robustness

Slide 8

Slide 8 text

BAD ARCHITECTURE Fear of change Lots of WTFs Hacks Mystic knowledge

Slide 9

Slide 9 text

What main features should your architecture have to enable you to write UI tests effectively?

Slide 10

Slide 10 text

Each layer must have a crystal clear purpose and responsibilities

Slide 11

Slide 11 text

Be able to replace the dependencies to inject test doubles

Slide 12

Slide 12 text

Disclaimer: Viper-ish

Slide 13

Slide 13 text

UI PRESENTER USE CASE NAVIGATOR REPOSITORY DATA SOURCE … N … COMPOSITION ROOT COMPOSITION ROOT A … N …

Slide 14

Slide 14 text

UI PRESENTER USE CASE NAVIGATOR REPOSITORY DATA SOURCE … N COMPOSITION ROOT COMPOSITION ROOT A … N … …

Slide 15

Slide 15 text

UI Abstraction over UIViewController Passive Views

Slide 16

Slide 16 text

Use Cases Gateway to the “data” world Expose what your domain layer can do

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

CompositionRoot Knows how to instantiate every collaborator of the app Knows about the particulars of a collaborator

Slide 19

Slide 19 text

Presenter Tell the UI what to do Tell Navigators where the user wants to go Execute Use Cases KEY point for dependency injection

Slide 20

Slide 20 text

How does this look like in a real app?

Slide 21

Slide 21 text

FoursquareTop

Slide 22

Slide 22 text

App launch

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

DEPENDENCIES RootNavigator func installRootViewController(window: UIWindow) { let vc = appCompositionRoot.getInitialViewController() currentNavigationController = vc window.rootViewController = vc }

Slide 25

Slide 25 text

DEPENDENCIES AppCompositionRoot class AppCompositionRoot { let venue: VenueCompositionRoot func getInitialViewController() -> UINavigationController { let vc = venue.getBestVenuesAroundViewController() return UINavigationController(rootViewController: vc) } }

Slide 26

Slide 26 text

DEPENDENCIES VenueCompositionRoot func getBestVenuesAroundViewController() -> UIViewController { let vc = BestVenuesAroundYouViewController() vc.venuesAroundYouPresenter = BestVenuesAroundYouPresenter( ui: vc, getBestPlacesAroundYouUseCase: getPlacesAroundYouUseCase(), getUserLocationUseCase: getUserLocationUseCase(), navigator: getVenueListNavigator() ) return vc }

Slide 27

Slide 27 text

Going to a venue detail

Slide 28

Slide 28 text

TAP

Slide 29

Slide 29 text

presenter.venueSelected(venue) UI navigator.goTo(venue) PRESENTER compositionRoot.getVC(venue) pushViewController() NAVIGATOR

Slide 30

Slide 30 text

Screen BestVenuesAroundYouViewController func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { guard let venue = ds.venue(atIndexPath: indexPath) else { return } venuesAroundYouPresenter.venueSelected(venue) }

Slide 31

Slide 31 text

Screen BestVenuesAroundYouPresenter func venueSelected(venue: VenueViewModel) { navigator.goTo(venueDetail: venue) }

Slide 32

Slide 32 text

DEPENDENCIES VenueListNavigator func goTo(venueDetail venue: VenueViewModel) { let vc = appCompositionRoot.venue.getVenueDetailViewController( forVenue: venue ) currentNavigationController?.pushViewController( vc, animated: true ) }

Slide 33

Slide 33 text

Testing this

Slide 34

Slide 34 text

STRATEGY

Slide 35

Slide 35 text

UI PRESENTER USE CASE NAVIGATOR REPOSITORY DATA SOURCE … N … COMPOSITION ROOT COMPOSITION ROOT A … N …

Slide 36

Slide 36 text

TEST main.swift let appDelegateClass = Environment().isInTestingRun() ? NSStringFromClass(AppDelegateForTests) : NSStringFromClass(AppDelegate) UIApplicationMain( Process.argc, Process.unsafeArgv, nil, appDelegateClass )

Slide 37

Slide 37 text

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 }

Slide 38

Slide 38 text

TEST BaseUITest func openViewController() { let appCompositionRoot = getTestingAppCompositionRoot( baseAppCompositionRoot ) rootNavigator = RootNavigator( appCompositionRoot: appCompositionRoot ) presentViewController( keyWindow, navigator: rootNavigator, appCompositionRoot: appCompositionRoot ) } 1 2 3

Slide 39

Slide 39 text

Test Cases

Slide 40

Slide 40 text

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() }

Slide 41

Slide 41 text

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 }

Slide 42

Slide 42 text

TEST func getGetUserLocationUseCase() -> GetUserLocationUseCase { return GetUserLocation( gps: getGPS(), repository: getVenueDataSource() ) } func getGetUserLocationUseCase() -> GetUserLocationUseCase { return stubGetUserLocationUseCase } VenueCompositionRoot TestingVenueCompositionRoot

Slide 43

Slide 43 text

TEST BestVenuesAroundYouViewControllerTests func testLoadingIndicatorIsVisibleWhenLoadingVenues() { givenWeAreFetchingUsersLocation() openViewController() waitForViewWithLocalizedAccessibilityLabel(.Loading) } override func presentViewController(window: UIWindow, navigator: RootNavigator, appCompositionRoot: AppCompositionRoot) { presentViewController( appCompositionRoot.venue.getBestVenuesAroundViewController() ) }

Slide 44

Slide 44 text

TEST BestVenuesAroundYouViewControllerTests func testOpenSettingsIfTheGPSAccessIsDeniedAndTheUserOpensSettings() { givenThereWillBeAnErrorFetchingUserLocation(error: .AccessDenied) openViewController() tapGoToSettings() expect( stubVenueListNavigator.didNavigateToSettings ).toEventually( equal(true) ) } GIVEN WHEN THEN

Slide 45

Slide 45 text

TEST BestVenuesAroundYouViewControllerTests func testRetryShowsVenuesAfterThereIsAnErrorFetchingUsersLocation() { getUserLocationUseCase.givenThereWillBeAnErrorFetchingUsersLocation() openViewController() expectCanNotFetchLocationError(toBeVisible: true) getBestPlacesAroundYouUseCase.givenThereWillBeVenues() tapRetry() expectVenueList( getBestPlacesAroundYouUseCase.venueList ) expectCanNotFetchLocationError(toBeVisible: false) }

Slide 46

Slide 46 text

Testing the real navigation

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

TEST BestVenuesAroundYouViewControllerNavigationTests func testNavigatesToVenueDetailWhenUserTapsOnVenue() { getVenuesUseCase.givenThereWillBeVenues() let topVenue = getVenuesUseCase.getTopVenue() getVenueDetailsUseCase.venue = topVenue openViewController() tapTopVenue() expectTitle(ofVenue: topVenue, toBeVisible: true) } GIVEN WHEN THEN

Slide 49

Slide 49 text

Why is all this possible?

Slide 50

Slide 50 text

Thanks!

Slide 51

Slide 51 text

Questions?