Hello, espresso! Part 5 Automating WebViews πΈοΈπ
Learn how to automate WebViews in hybrid android apps using espresso-web
In the last part Hello, espresso! Part 4 Working with Idling resources π΄, We understood how to useΒ espresso idling resourcesΒ to achieve synchronization when the app is running background tasks that espresso is not aware of. Go ahead and have a read in case you missed it.
Automating Hybrid apps with WebViews
Switching gears to a different topic now, many android apps expose certain business logic Β WebViews
Β within Native apps. You can think of it as a mobile browser like chrome/firefox that is able to render web pages.
Itβs quite important to cover the interaction your native app has with these Web components to ensure there are no missed regressions in your app releases.
Some common examples of when an app might have a WebView are:
There may be a help or support pages for your business that needs to be dynamic and change whenever a new page/option is added
Or pages to host marketing content are launched
Or even lengthy terms and conditions for a new feature
Having a WebView is great since its much faster to make changes and roll out web app changes to production and developers are not restricted by Google play store approvals and rollouts
What tests to cover and what not? π€
Espresso framework provides a libraryΒ espresso-web
Β that provides an "espresso like" API interface overΒ Selenium WebDriverΒ API
When should you write an espresso web test? β
If your app has user journeys between a native app and web view, and you want to make sure theseΒ interactionsΒ work then it surely makes sense to cover this as part of an Espresso test
When you should not? β
However, if you only want toΒ verify theΒ WebView
Β content or functionalityΒ without any native interaction checks, then itβs much better to write anΒ automated Web TestΒ instead of using Selenium WebDriver or equivalent library that would be much faster and easier to write, maintain and run
How does this work?
Espresso web uses aΒ Javascript bridgeΒ to interact with the WebDriver framework and usesΒ atoms
You can consider atoms is equivalent toΒ
ViewMatcher
Β andΒViewAction
Β classesEspresso web wraps these atoms withΒ
Web
Β andΒWeb.WebInteraction
Β classes that make writing an Espresso web test feel similar to writing a test for a native app
we useΒ findElement()
Β orΒ getElement()
Β methods with certain locators (likeΒ ID, XPATH, CSS, CLASS NAMEΒ etc) to find an element and then perform an action or assertion on top of it
Writing your first Web test
Let's write a test to see this in action
Adding dependencies
We need to addΒ espresso-web
Β to ourΒ app/build.gradle
Β file
androidTestImplementation 'androidx.test:core:' + rootProject.coreVersion;
androidTestImplementation 'androidx.test.ext:junit:' + rootProject.extJUnitVersion;
androidTestImplementation 'androidx.test:runner:' + rootProject.runnerVersion;
androidTestImplementation 'androidx.test:rules:' + rootProject.rulesVersion;
androidTestImplementation 'androidx.test.espresso:espresso-web:' + rootProject.espressoVersion;
And add below library versions in the rootΒ build.gradle
Β file
ext {
buildToolsVersion = "31.0.0"
androidxAnnotationVersion = "1.2.0"
guavaVersion = "30.1.1-android"
coreVersion = "1.4.1-alpha05"
extJUnitVersion = "1.1.4-alpha05"
runnerVersion = "1.5.0-alpha02"
rulesVersion = "1.4.1-alpha05"
espressoVersion = "3.5.0-alpha05"
}
Understanding app under test
Let's understand the app flows that we would be automating before we jump into the code
Feature: Test WebView inside Native app using espresso
Scenario: When user enters text and taps on Change text
GIVEN the user is on the home page of BasicEspressoWebSample
WHEN the user enters a text in textbox
AND the user taps on "Change Text" button
THEN the label displays the entered test
Scenario: When user enters text and taps on Change text
GIVEN the user is on the home page of BasicEspressoWebSample
WHEN the user enters a text in textbox
AND the user taps on "Change Text And Submit" button
THEN the web page redirects to another page with a label displaying the entered test
Below screens show the app for the above scenarios:
Above Figure: WHEN the user enters a text in the textbox
Above Figure: AND the user taps on the "Change Text" button THEN the label displays the entered text
Above Figure: THEN the web page redirects to another page with a label displaying the entered test
Finding locators using Chrome debug tools
If you try to use the Android studioΒ "Layout Inspector", then you'll be disappointed to see that it does not show the Component tree-like native apps to enable us to find the desired locator, it only shows the containing WebView
Above Figure: Trying to use layout inspector
So, whatβs the solution?
We need to useΒ Chrome remote debugging toolΒ to inspect our app.
Follow the below steps:
Connect your device or emulator via USB
Ensure developer options are enabled
Verify that USB debugging is enabled and the connected laptop trusts your android device
Once done, you can typeΒ chrome://inspect
Β in your chrome browser tab and this would open chrome debugging tools like below, Tap onΒ InspectΒ button
Above Figure: Chrome inspects devicesβ screen
Once you tap on inspecting it should bring upΒ the Chrome developer toolsΒ window and you can go toΒ the ElementsΒ tab and then use the inspect button to look at the HTML/CSS structure of the web page
Above Figure: Chrome dev tools showing the review and its associated DOM for the home page
Once you enter some text and tap on the "Change text and submit" button, you'll see a screen like below
Above Figure: Chrome dev tools showing the 2nd web page after the user taps on "Change text and Submit"
Enabling Chrome debugging + JavaScript (JS) Execution on the app
We need to add a few lines in our app source code to enable chrome debugging.
Please ensure you add the line:Β WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG);
Β in yourΒ MainActivity, this would enable Chrome to debug tools to inspect your WebView only forΒ debugΒ versions of the app
This method was also added inΒ the Android Kit katΒ thus we need to add an annotation likeΒ @RequiresApi(api = Build.VERSION_CODES.KITKAT)
Β to ourΒ onCreate
Β method as well
We also add additional logic to enable JS execution
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.loadUrl(urlFromIntent(getIntent()));
mWebView.requestFocus();
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return false;
}
});
Below you can see the completeΒ onCreate()
Β method
// setWebContentsDebuggingEnabled requires KITKAT version+, thus adding @RequiresApi annotation
// Also suppressing warning about enabling Javascript execution that can cause XSS vulnerabilities
@SuppressLint("SetJavaScriptEnabled")
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_web_view);
mWebView = (WebView) findViewById(R.id.web_view);
// Enables chrome remote debugging:
// https://stackoverflow.com/questions/21903934/how-to-debug-webview-remotely
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG);
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.loadUrl(urlFromIntent(getIntent()));
mWebView.requestFocus();
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return false;
}
});
}
private static String urlFromIntent(@NonNull Intent intent) {
checkNotNull(intent, "Intent cannot be null!");
String url = intent.getStringExtra(KEY_URL_TO_LOAD);
return !TextUtils.isEmpty(url) ? url : WEB_FORM_URL;
}
If you are not familiar with how to findΒ web locators, search with keywords "locators xpath" or "locators css" in google and you should be able to find tons of resources.
Alternatively, You could refer toΒ Selenium Tips: Better Locators in SeleniumΒ post from Sauce Labs as a starter
Let's write our test
You can see the complete test below:
package com.example.android.testing.espresso.web.BasicSample;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.web.assertion.WebViewAssertions.webMatches;
import static androidx.test.espresso.web.sugar.Web.onWebView;
import static androidx.test.espresso.web.webdriver.DriverAtoms.clearElement;
import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement;
import static androidx.test.espresso.web.webdriver.DriverAtoms.getText;
import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick;
import static androidx.test.espresso.web.webdriver.DriverAtoms.webKeys;
import static org.hamcrest.Matchers.containsString;
import androidx.test.espresso.web.webdriver.Locator;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class WebViewPracticeTest {
// Launch activity with ActivityScenarioRule
@Rule
public ActivityScenarioRule<WebViewActivity> mActivityScenarioRule = new ActivityScenarioRule<>(WebViewActivity.class);
@Before
public void enableJSOnWebView() {
// We enable Javascript execution on the webview
onWebView().forceJavascriptEnabled();
}
@Test
public void whenUserEntersText_AndTapsOnChangeTextAndSubmit_ThenTextIsChangedInANewPage() {
String text = "Peekaboo";
// We start our test by finding the WebView we want to work with
onWebView(withId(R.id.web_view))
// Find the text box using id
.withElement(findElement(Locator.ID, "text_input"))
// Clear any existing text to ensure predictable result
.perform(clearElement())
// Type text
.perform(webKeys(text))
// Find submit button
.withElement(findElement(Locator.ID, "submitBtn"))
// Click on submit button
.perform(webClick())
// Find the element where the changed text is displayed
.withElement(findElement(Locator.ID, "response"))
// Verify the text for the element has our initially entered text
.check(webMatches(getText(), containsString(text)));
}
/**
* This test repeats steps as above, it just clicks on change text button and verifies the
* result text is updated on the same page
*/
@Test
public void whenUserEntersText_AndTapsOnChange_ThenTextIsChangedInANewPage() {
String text = "Peekaboo";
onWebView(withId(R.id.web_view))
.withElement(findElement(Locator.ID, "text_input"))
.perform(clearElement())
.perform(webKeys(text))
.withElement(findElement(Locator.ID, "changeTextBtn"))
.perform(webClick())
.withElement(findElement(Locator.ID, "message"))
.check(webMatches(getText(), containsString(text)));
}
}
Let's unpack this and understand in more detail
We start by launching our Activity usingΒ ActivityScenarioRule
Β and specify it as a JUnit rule
// Launch activity with ActivityScenarioRule
@Rule
public ActivityScenarioRule<WebViewActivity> mActivityScenarioRule = new ActivityScenarioRule<>(WebViewActivity.class);
Since we are using Javascript to drive the browser, we need to enable it on the WebView, To do so, we'll add a method to run before any test and annotate it withΒ @Before
@Before
public void enableJSOnWebView() {
// We enable Javascript execution on the webview
onWebView().forceJavascriptEnabled();
}
To start our web test, we first specify theΒ WebView
Β we want to work using theΒ onWebView()
Β method
If we had a single WebView on the activity then we could skip adding theΒ
withId()
Β method (Here we keep it to be specific), this method returns aΒWebInteraction
Β object that exposes the Web API actions to drive ourΒWebView
// We start our test by finding the WebView we want to work with
onWebView(withId(R.id.web_view))
Next, we want to be able to find our text box
We do so by usingΒ
withElement()
Β the method that takes anΒAtom<ElementReference>
Β as input to find an element,We useΒ
findElement()
Β that takes the first argument as the Locator, followed by the actual locatorIn our test, we have anΒ
id
Β available for these elements by looking in the DOM tree in Chrome debug tools, we mentionΒtext_input
We could use any of the below locators to identify our elements:
CLASS_NAME("className"),
CSS_SELECTOR("css"),
ID("id"),
LINK_TEXT("linkText"),
NAME("name"),
PARTIAL_LINK_TEXT("partialLinkText"),
TAG_NAME("tagName"),
XPATH("xpath");
The full statement looks like the below:
// Find the text box using id
.withElement(findElement(Locator.ID, "text_input"))
We want to make sure we clear any existing text from the text box and type the desired text, we do so usingΒ clearElement()
Β andΒ webKeys(text)
Β method:
// Clear any existing text to ensure predictable result
.perform(clearElement())
// Type text
.perform(webKeys(text))
We then use similar methods to find the button element and perform a click on it
// Find submit button
.withElement(findElement(Locator.ID, "submitBtn"))
// Click on submit button
.perform(webClick())
And finally, we check that the label has the desired text that we entered by adding:
// Find the element where the changed text is displayed
.withElement(findElement(Locator.ID, "response"))
// Verify the text for the element has our initially entered text
.check(webMatches(getText(), containsString(text)));
If you notice, most of the methods have a similar structure to native app espresso actions and assertions, most of our methods have "web" prefix, you could use this intuition to find the relevant methods using Android studio for your specific test cases
We can then easily write the 2nd test using similar commands (see the above)
If we run both these tests, we should see them launch our activity and drive ourΒ WebView
Β similar to a native app. Hopefully, you understand the general structure well enough to go ahead and write your own tests.
Resources π
You can find the app and test code for this post on Github underΒ
automationhacks/testing-samples
:Please readΒ Espresso WebΒ on android developers for some more context
Conclusion β
Hopefully, this post gives you an idea of how to work within espresso. Stay tuned for the next post where weβll dive into another interesting espresso capabiility
As always, Do share this with your friends or colleagues and if you have thoughts or feedback, Iβd be more than happy to chat over on Twitter or comments. Until next time. Happy Testing and learning.