Espresso-friendly Bottom Sheet interactions

December 21, 2020 , ,

Like many other integration testing frameworks, Android’s Espresso provides a management layer for asynchronous operations. Correct usage of this layer allows the test author to treat long-running activities as if they were synchronous, resulting in reliable UI tests. In this post, we’ll describe how we used Espresso to build reliable tests for one such behavior — Android’s animated Bottom Sheets.

Asynchronous UI

To use Espresso’s asynchronous management layer, engineers must identify any long-running operations that their application may perform during tests. Those behaviors are then registered with Espresso, which will automatically “idle” as necessary to await their completion. 

As you may imagine, these “idling” operations are often data tasks like API calls or local file I/O. But they can be UI animations as well — for example, we may need the test framework to idle until an animated screen transition completes, so that it doesn’t attempt to click a button before the next screen is visible.

To get around most animation problems in Espresso, we can just globally disable animations in the Android OS before starting our test. Yet even in this configuration there are UI elements that can move asynchronously.

Sheet states: collapsed
and expanded

Bottom Sheet interactions

Bottom Sheets are unusual in that, instead of performing discrete animations (like a screen transition) they follow the path of the user’s finger continuously. This is true until the Bottom Sheet is dragged to a particular point on the screen — upon which the Sheet will “snap” into a discrete state there. 

Bottom Sheet in motion

Like many gestures that attempt to interpret user intention, these “drag-and-snap” interactions are quite delicate in terms of timing and magnitude, and are challenging to perform in an automated way. While Espresso can simulate generic swiping interactions via swipeUp and swipeDown, we found this solution too imprecise for transitioning between Sheet states. Would the swipe have enough magnitude to fully expand the Bottom Sheet, even on taller screens? What if the swipe was performed too quickly or too slowly, due to device lag? Other solutions felt overly complex: a custom dragging action would need to perform screen measurements and tricky pixel math, besides being subject to the same timing concerns as swipeUp

Reliable state transitions

We decided to create an Idling Resource that is capable of monitoring a Bottom Sheet’s state, paired with custom Espresso ViewActions which manipulate that state. Let’s look at the View Action first:

As you can see, this action takes a matched Bottom Sheet view, instantiates a BottomSheetBehavior object with it, and manipulates the Sheet’s state via code. Programmatically snapping the Sheet into the desired state avoids the pitfalls of relying on the delicate “drag-to-snap” UI interactions described above.

The importance of reliability

The imprecision of our earlier solutions produced false-negative test results, which frustrated developers who needed to investigate and perhaps re-run each failure. We have a large (400+ case) integration suite run by several Android developers many times per day, so even a seemingly minor source of test flakiness can accumulate into a major productivity problem.

Tradeoffs

It should be noted that this approach strays a bit from the idiomatic Espresso style. Ideal “black box” integration tests simulate a human user’s interactions with the app, remaining agnostic of the code’s implementation details. In our example, an idiomatic Bottom Sheet test would be driven only by events that approximate what a real user’s finger would do, and avoid programmatic solutions like BottomSheetSetStateAction

By manipulating BottomSheetBehavior in code, we see a loss in test fidelity equal to the size of our shortcut: we implicitly trust that the “drag-to-snap” functionality of Bottom Sheets will work properly in our Production app. Due to the high stability of the Bottom Sheet library, and the significant reliability gains that came with programmatic state manipulation, we decided that this tradeoff was worth making.

Waiting for Sheets to settle

With BottomSheetSetStateAction in place, our next step was instructing Espresso how to await the completion of Bottom Sheet state changes. Consider the below class, which defines the “glue” between a BottomSheetCallback and the Espresso IdlingResource framework.

By implementing the isDesiredState method, we can customize which Bottom Sheet end state(s) we wish to consider. At Wealthfront, we ended up with two extensions of this pattern — one which waits for a Bottom Sheet to “settle” (state != STATE_DRAGGING && state != STATE_SETTLING) and another which waits for a particular state:

Putting it all together

Let’s look at an outline for using our Idling Resources and BottomSheetSetStateAction in a test. First we’ll get a reference to the Activity under test, and use it to get a reference to the Bottom Sheet we want to interact with (note: this step may vary depending on how your application is architectured). From there, we’ll wait until the Bottom Sheet is ready to be interacted with, i.e. that it has animated into the “peek” state (STATE_COLLAPSED). Then, we’ll invoke BottomSheetSetStateAction to move our Sheet to the expanded (STATE_EXPANDED) state. After waiting for this transition to finish, we’ll finally click a button nested inside the expanded Sheet’s UI. Throughout we use a helper method, withBottomSheetResource, to make these interactions more legible and to ensure that our Idling Resources are unregistered once they are no longer needed.

Conclusion

The delicate, continuous nature of Bottom Sheet animations makes them challenging to interact with in an automated way. Creating a programmatic solution for Bottom Sheet state changes, and then wrapping those interactions with Idling Resources, dramatically improved the stability of our related integration tests. At Wealthfront, our QA process is entirely automated, so it’s very important for us to build scalable, reliable, and effective test coverage.

Automated test in motion. Expands collapsed Bottom Sheet, clicks Learn More, verifies effect

Hopefully this pattern helps you build reliable automated Bottom Sheet interactions in your own codebase! And if you found this interesting, take a look at our careers page and come help Wealthfront build amazing financial products.

Disclosure

We’ve partnered with Green Dot Bank, Member FDIC, to bring you checking features.

Checking features for the Cash Account are subject to identity verification by Green Dot Bank. Debit Card is optional and must be requested. Wealthfront Cash Account Visa® Debit Card is issued by Green Dot Bank, Member FDIC, pursuant to a license from Visa U.S.A. Inc. Visa is a registered trademark of Visa International Service Association. Green Dot Bank operates under the following registered trade names: GoBank, Green Dot Bank and Bonneville Bank. All of these registered trade names are used by, and refer to, a single FDIC-insured bank, Green Dot Bank. Deposits under any of these trade names are deposits with Green Dot Bank and are aggregated for deposit insurance coverage. Wealthfront products and services are not provided by Green Dot Bank. Green Dot is a registered trademark of Green Dot Corporation. ©2020 Green Dot Corporation. All rights reserved. Other fees apply to the checking features. Fee-free ATM access applies to in-network ATMs only. For out-of-network ATMs and bank tellers a $2.50 fee will apply, plus any additional fee that the owner or bank may charge. Please see the Deposit Account Agreement for details.

Cash Account is offered by Wealthfront Brokerage LLC (“Wealthfront Brokerage”), a member of FINRA/SIPC. Neither Wealthfront Brokerage nor any of its affiliates are a bank, and Cash Account is not a checking or savings account. We convey funds to partner banks who accept and maintain deposits, provide the interest rate, and provide FDIC insurance. Investment management and advisory services are provided by Wealthfront Advisers LLC (“Wealthfront Advisers”), an SEC registered investment adviser, and financial planning tools are provided by Wealthfront Software LLC (“Wealthfront”).

The cash balance in the Cash Account is swept to one or more banks (the “program banks”) where it earns a variable rate of interest and is eligible for FDIC insurance.  FDIC insurance is not provided until the funds arrive at the program banks. FDIC insurance coverage is limited to $250,000 per qualified customer account per banking institution. Wealthfront uses more than one program bank to ensure FDIC coverage of up to $1 million for your cash deposits.  For more information on FDIC insurance coverage, please visit www.FDIC.gov. Customers are responsible for monitoring their total assets at each of the program banks to determine the extent of available FDIC insurance coverage in accordance with FDIC rules. The deposits at program banks are not covered by SIPC. 

The APY may change at any time, before or after the Cash Account is opened. The APY for the Wealthfront Cash Account represents the weighted average of the APY on the aggregate deposit balances of all clients at the program banks. Deposit balances are not allocated equally among the participating program banks.

The information contained in this communication is provided for general informational purposes only, and should not be construed as investment or tax advice. Nothing in this communication should be construed as tax advice, a solicitation or offer, or recommendation, to buy or sell any security. Any links provided to other server sites are offered as a matter of convenience and are not intended to imply that Wealthfront Advisers or its affiliates endorses, sponsors, promotes and/or is affiliated with the owners of or participants in those sites, or endorses any information contained on those sites, unless expressly stated otherwise.

Wealthfront, Wealthfront Advisers and Wealthfront Brokerage are wholly owned subsidiaries of Wealthfront Corporation.

© 2020 Wealthfront Corporation. All rights reserved.