Slide 1

Slide 1 text

UI Tests are Easy to Write said no Android dev ever Kevin Lai & Valera Zakharov

Slide 2

Slide 2 text

Why?

Slide 3

Slide 3 text

Aug 2016 Nov 2017

Slide 4

Slide 4 text

1. No time 2. Too hard to develop 3. Not enough knowledge

Slide 5

Slide 5 text

2011

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

Any test is good... … until you start running it in CI

Slide 11

Slide 11 text

2013

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

API for Test Authors onView(withId(R.id.greeting)) .perform(click()); onView(withText("Hello Droidcon SF!")) .check(matches(isDisplayed()));

Slide 14

Slide 14 text

2014

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

2015

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

2016

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

https://academy.realm.io/posts/writing-solid-ui-tests-on-android-mdevcamp-zakharov

Slide 26

Slide 26 text

UI tests can still be Targeted & Hermetic

Slide 27

Slide 27 text

2017

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

Today

Slide 30

Slide 30 text

At Slack

Slide 31

Slide 31 text

Android Test Pyramid at Slack 100ish e2e 555 functional UI 3448 unit

Slide 32

Slide 32 text

Developer Feedback Workflow Ease of Testing App Quality Release Process “Writing quality UI tests is still a difficult thing to do. It would be great to have some time put into building up a base to work from” 2.5/4

Slide 33

Slide 33 text

http://jakewharton.com/testing-robots

Slide 34

Slide 34 text

What How How What Presenter / Components Model View Robot Test UI Test Architecture

Slide 35

Slide 35 text

I’m going on a UI test adventure

Slide 36

Slide 36 text

The Problem

Slide 37

Slide 37 text

What How How What Presenter / Components Model View Robot Test Test

Slide 38

Slide 38 text

Lots of Components Data Providers - fetch and cache Persistence - cache data Managers - app-wide process or state (e.g. message sending, channel syncing) Helpers - data massaging API Actions - REST and websocket requests Presenters? Great!

Slide 39

Slide 39 text

Repetitive Mocking - User User user = User.builder().../* setters */...build(); when(mockUsersDataProvider.getUser(USER_ID)) .thenReturn(Observable.just(user)); when(mockUserPresenceManager.isUserActive(USER_ID, true)) .thenReturn(true); when(mockDndInfoDataProvider.getDndInfo(userId)) .thenReturn(Observable.just(new DndInfo(/* params */)));

Slide 40

Slide 40 text

Repetitive Mocking - Channel Channel channel = Channel.builder().../* setters */...build(); when(mockChannelsDataProvider.getChannelObservable(CHANNEL_ID)) .thenReturn(Observable.just(channel)); when(mockChannelNameHelper.getDisplayNameObservable(eq(channel))) .thenReturn(Observable.just(CHANNEL_NAME));

Slide 41

Slide 41 text

@Before @Override public void setUp() throws Exception { super.setUp(); User user = User.builder().../* setters */...build(); when(mockUsersDataProvider.getUser(USER_ID)) .thenReturn(Observable.just(user)); when(mockUserPresenceManager.isUserActive(USER_ID, true)) .thenReturn(true); when(mockDndInfoDataProvider.getDndInfo(userId)) .thenReturn(Observable.just(new DndInfo(/* params */))); Channel channel = Channel.builder().../* setters */...build(); when(mockChannelsDataProvider.getMessagingChannelObservable(CHANNEL_ID)) .thenReturn(Observable.just(channel)); when(mockChannelNameHelper.getDisplayNameObservable(eq(channel))) .thenReturn(Observable.just(CHANNEL_NAME)); launchActivity(); }

Slide 42

Slide 42 text

The Solution

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

What How How What Presenter / Components Model View Robot Test Test

Slide 45

Slide 45 text

How How What Robot Test View Model Presenter / Components What

Slide 46

Slide 46 text

Power Pack

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

How Robot Test View How PowerPack What Presenter / Components Model How What

Slide 49

Slide 49 text

Set up Models not Components slackPowerPack.usersPack() .addUser(User.builder().../* setters */...build()) .addUser(User.builder().../* setters */...build()) .pack(); slackPowerPack.channelsPack() .addChannel(Channel.builder().../* setters */...build()) .addChannel(Channel.builder().../* setters */...build()) .addChannel(Channel.builder().../* setters */...build()) .pack();

Slide 50

Slide 50 text

Under the hood public UsersPack.Builder usersPack() { return UsersPack.builder() .dndInfoDataProvider(mockDndInfoDataProvider) .persistentStore(mockPersistentStore) .usersDataProvider(mockUsersDataProvider) .userPresenceManager(mockUserPresenceManager); }

Slide 51

Slide 51 text

public class UsersPack { public void pack() { UsersPack usersPack = build(); UsersDataProvider usersDataProvider = usersPack.usersDataProvider(); UserPresenceManager userPresenceManager = usersPack.userPresenceManager(); DndInfoDataProvider dndInfoDataProvider = usersPack.dndInfoDataProvider(); // More components... List users = usersPack.users(); for (User user : users) { when(usersDataProvider.getUser(user.id())) .thenReturn(Observable.just(user)); when(userPresenceManager.isUserActive(user.id(), true)) .thenReturn(true); when(dndInfoDataProvider.getDndInfo(user.id())) .thenReturn(Observable.just(new DndInfo(/* params */))); // More mocking... } } }

Slide 52

Slide 52 text

Under the hood public ChannelsPack.Builder channelsPack() { return ChannelsPack.builder() .accountManager(mockAccountManager) .featureFlagStore(mockFeatureFlagStore) .lastOpenedMsgChannelIdStore(mockLastOpenedMsgChannelIdStore) .mpdmDisplayNameHelper(mockMpdmDisplayNameHelper) .persistentStore(mockPersistentStore) .prefsManager(mockPrefsManager) .slackApi(mockSlackApi) .channelListDataProvider(mockChannelListDataProvider) .channelNameHelper(mockChannelNameHelper) .channelsPaneDataProvider(mockChannelsPaneDataProvider) .channelsDataProvider(mockChannelsDataProvider) .messageCountManager(mockMessageCountManager) .notificationPrefsDataProvider(mockNotificationPrefsDataProvider) .notificationPrefsManager(mockNotificationPrefsManager) .userChannelListDataProvider(mockUserChannelListDataProvider);

Slide 53

Slide 53 text

@Before @Override public void setUp() throws Exception { super.setUp(); slackPowerPack = new SlackPowerPack(context.getApplicationContext()); slackPowerPack.usersPack() .addUser(User.builder().../* setters */...build()) .addUser(User.builder().../* setters */...build()) .pack(); slackPowerPack.channelsPack() .addChannel(Channel.builder().../* setters */...build()) .addChannel(Channel.builder().../* setters */...build()) .addChannel(Channel.builder().../* setters */...build()) .pack(); launchActivity(); }

Slide 54

Slide 54 text

Wins ● *Pack classes encapsulate all mockito statements ● Tests benefit from updates in one place ● Test authors focus on writing their actual test ● Fluent APIs

Slide 55

Slide 55 text

Developer Feedback Workflow Ease of Testing App Quality Release Process “Ease of test writing: this has improved a lot. It would be nice to take the time to update some of our old tests to use PowerPack/our latest patterns." 2.5/4 2.92/4 “I would also love an easy way to set up a team.”

Slide 56

Slide 56 text

Focus on the pattern

Slide 57

Slide 57 text

No content

Slide 58

Slide 58 text

The Solution ++

Slide 59

Slide 59 text

@Before @Override public void setUp() throws Exception { super.setUp(); User user = User.builder().../* setters */...build(); when(mockUsersDataProvider.getUser(USER_ID)) .thenReturn(Observable.just(user)); when(mockUserPresenceManager.isUserActive(USER_ID, true)) .thenReturn(true); when(mockDndInfoDataProvider.getDndInfo(userId)) .thenReturn(Observable.just(new DndInfo(/* params */))); Channel channel = Channel.builder().../* setters */...build(); when(mockChannelsDataProvider.getMessagingChannelObservable(CHANNEL_ID)) .thenReturn(Observable.just(channel)); when(mockChannelNameHelper.getDisplayNameObservable(eq(channel))) .thenReturn(Observable.just(CHANNEL_NAME)); launchActivity(); }

Slide 60

Slide 60 text

@Before @Override public void setUp() throws Exception { super.setUp(); slackPowerPack = new SlackPowerPack(context.getApplicationContext()); slackPowerPack.usersBuilder() .addUser(User.builder().../* setters */...build()) .addUser(User.builder().../* setters */...build()) .pack(); slackPowerPack.channelsBuilder() .addChannel(Channel.builder().../* setters */...build()) .addChannel(Channel.builder().../* setters */...build()) .addChannel(Channel.builder().../* setters */...build()) .pack(); launchActivity(); }

Slide 61

Slide 61 text

@Before @Override public void setUp() throws Exception { super.setUp(); slackPowerPack = new SlackPowerPack(context.getApplicationContext(), "team_a_config.json"); launchActivity(); }

Slide 62

Slide 62 text

JSON Config { “logged_in_user”: String “team”: { } “users”: [ ], “channels”: [ ], “history”: [ { “channel_id”: String “messages”: [ ] } ] }

Slide 63

Slide 63 text

public class WorkspaceConfig { private String logged_in_user; private Team team; private List channels; private List dms; private List users; private List history; }

Slide 64

Slide 64 text

2018

Slide 65

Slide 65 text

{ "ok": true, "channels": [ { "id": "C0G9QF9GW", "name": "random", "is_channel": true, "created": 1449709280, "creator": "U0G9QF9C6", "is_archived": false, "is_general": false, "name_normalized": "random", "is_shared": false, "is_org_shared": false, "is_member": true, "is_private": false, "is_mpim": false, "members": [ "U0G9QF9C6", "U0G9WFXNZ" ], "topic": { "value": "Other stuff", "creator": "U0G9QF9C6", "last_set": 1449709352 }, "previous_names": [], "num_members": 2 }, JSON Config { “logged_in_user”: String “team”: { } “users”: [ ], “channels”: [ ], “history”: [ { “channel_id”: String “messages”: [ ] } ] }

Slide 66

Slide 66 text

Charging the Power Pack E2E Test record API Data Replay Test (e.g. Perf) Real Backend Hermetic Functional Test profit replay generate fetch

Slide 67

Slide 67 text

@valera_zakharov @kevin_lai14