E2E Test Framework
Introduction
E2E Test
E2E Test is a software quality assurance methodology that checks if an App performs as designed on all levels and across all subsystems. The scope of E2E Test encompasses the App in its entirety, as well as its integration with external interfaces and outside Apps.
E2E Test
For example, a simplified E2E Test of an email App might involve
- Logging in to the application
- Accessing the inbox
- Opening and closing the mailbox
- Composing, forwarding or replying to email
- Checking the sent items
- Logging out of the application
Test Runner
A test runner is the library or tool that picks up unit tests and settings, then executes them and writes the test results to the console or log files.
Headless Browser
A headless browser is a web browser without a graphical user interface.
Advantages of Headless Browser
- Faster
- More stable
Headless browsers avoid draw operations, which handle rendering of the UI and their various pixels on the screen.
Usages of Headless Browser
- Test automation in modern web Apps.
- Scraping web sites for data ( Especially for AJAX pages )
- Automating interaction of web pages
- Run testing in servers without screen
Some Headless Browsers
Google Chrome: support headless mode after version 59
Firefox: support headless mode since version 55 in linux, since version 56 in Windows and macOS
PhantomJS: a headless web browser using Webkit layout engine for rendering web pages and JavaScriptCore for executing scripted tests.
PC Automation Tools
NPM downloads in past 2 years
These automation tools are used to test Web App built on browser.
- Selenium
- PhantomJS
- Puppeteer
- ...
PC Web Automation Tools
PhantomJS Home Page
http://phantomjs.org/
We can divide them into two classes:
- Selenium
- Puppeteer
- PhantomJS
Cross broswer support
Focus one browser
PC Web Automation Tools
Selenium VS PhantomJS and Puppeteer
Selenium | PhantomJS & Puppeteer |
---|---|
Java, Python, Javascript... | Javascript |
Cross browser support | Only support one browser |
Without browser enginee | With browser enginee |
Cannot forbid UI | Support headless mode |
Slower | Faster |
Large community | Smaller community |
PhantomJS | Puppeteer |
---|---|
Suspended | Developing |
Old javascript syntax | All syntax supported by Chrome |
Lower performance | Higher performance |
Only headless mode | Headless and common mode |
Not real browser environment | Real browser environment |
PhantomJS VS Puppeteer
PhantomJS VS Puppeteer
Development status
Developer of PhantomJs noticed that
PhantomJS development is suspended until further notice.
That means:
- PhantomJS may no longer support newest javascript syntax, for example, async/await
- Browser may fail to test Apps with newest javascript features
PhantomJS VS Puppeteer
Performance
PhantomJS VS Puppeteer
Performance
Mobile Automation Tools
Mobile E2E Automation Tool
Macaca | Appium |
---|---|
PC, Android, IOS, Hybrid, Electron | PC, Android, IOS, Hybrid |
Java, Python, Javascript, ... | Java, Python, Javascript, ... |
Inspector, Recorder | Appium Desktop |
Android API > 17 | All |
Detailed document for JS | Detailed document |
Smaller community | Large community |
More useful tools | Less tools |
Mobile E2E Automation Tool
Recorder and Inspector
Macaca Recorder&Inspector | Appium Appium Desktop |
---|---|
Can only save script to Javascript | Can save script to Java, Python... |
Inspector cannot show coordinate of cursor | Real-time coordinate display |
Recoder can set assertions | Cannot set assertions |
Recorder supports click, select, input, swipe |
Mobile E2E Automation Tool
Problem
Zoom API is broken when testing in Hybrid App
For Appium, we can use multiAction to simulate zoom operation:
action1 = TouchAction()
action2 = TouchAction()
action1.long_press(x=centerX, y=centerY)\
.move_to(x=centerX + 50, y=centerY + 50)\
.wait(100).release()
action2.long_press(x=centerX, y=centerY)\
.move_to(x=centerX - 50, y=centerY - 50)\
.wait(100).release()
m_action.add(action1, action2)
m_action.perform()
Mobile E2E Automation Tool
Problem
For Macaca, no API supports MultiAction.
This problem is still difficult to solve.
Other problem with Macaca:
- Lack detailed documents for Python and other languages
- Many tools only support Javascript
- Difficult to search solutions
Finally, I choose Appium for automating.
Study Case
Background
Testing Object: Hybrid App
Goal: Automate E2E testing in Android Emulator
Environment:
PC: Ubuntu 16.04 with python 3.5
Emulator:
Nexus 5X API 23
Nexus 5X API 24
Nexus 9 API 24
Appium
Introduction
Install Server
Install Appium Server by npm:
npm install -g appium
If download appium-chromedriver timeout,
try to use proxy to download it.
Add parameter after command:
--chromedriver_cdnurl=http://npm.taobao.org/mirrors/chromedriver
Check Denpendencies
Install appium-doctor by npm:
npm install -g appium-doctor
Run appium-doctor command, supplying the --ios or --android flags to check dependencies
After all dependencies are installed, run appium command to start server
Install Python Client
Install Python client by pip:
pip install Appium-Python-Client
import appium Python client in python file:
from appium import webdriver
Connect to Server
Start your Android emulator
Set the desired_capability
desired_capabilities = {
'platformName': 'Android',
'platformVersion': 'X.X',
'deviceName': 'you-emulator-name',
'app': 'you-app.apk'
}
Text
Connect to Server
driver = webdriver.Remote('http://localhost:PORT/wd/hub', desired_capabilities)
PORT is your Appium Server Port, default 4723
Connect to Server
For Hybrid Apps, chromedriver is necessary,
add chromedriverExecutable to desired_capabilities:
desired_capabilities = {
'platformName': 'Android',
'platformVersion': 'X.X',
'deviceName': 'you-emulator-name',
'app': 'you-app.apk',
'chromedriverExecutable': 'your-chromedriver-path'
}
Text
Chromedriver version should match the WebView version of emulator. You can see version of WebView in chrome://inspect
Text
Appium Desktop
Download release package from
https://github.com/appium/appium-desktop/releases/tag/v1.6.3
For Ubuntu, download .AppIamge file, then make it executable and execute it:
chmod a+x appium-desktop-1.6.3-x86_64.AppImage
./appium-desktop-1.6.3-x86_64.AppImage
Text
default configure is ok
click to start server
click the button to create connection
set desired capabilities
click to start session
touch actions
other tools
App XML source
Chrome Dev Tool ( for Hybrid App )
Open Chrome, enter chrome://inspect
WebView version
click start inspect
Javascript source
Write Testcase
Assume that testing App has XML:
<android.widget.FrameLayout>
<android.widget.LinearLayout>
<android.widget.FrameLayout resource-id="android:id/content">
<android.webkit.WebView>
<android.webkit.WebView content-desc="hybrid-base">
<android.view.View content-desc="Welcome">
<android.widget.EditText content-desc="Phone Number">
<android.view.View content-desc="Verify Code">
<android.widget.EditText content-desc="6 Digits">
<android.widget.Button content-desc="GET SMS">
<android.widget.Button content-desc="NEXT">
<android.view.View resource-id="android:id/statusBarBackground">
<android.view.View resource-id="android:id/navigationBarBackground">
How to locate an element
1. Use Absolute XPATH
# return element whose text is 'Welcome'
driver.find_element_by_xpath('/hierarchy/android.widget.FrameLayout/ \
android.widget.LinearLayout/android.widget.FrameLayout/android.webkit.WebView/ \
android.webkit.WebView/android.view.View[@content-desc="Welcome"]')
- Easy
- Absolute XPATH will be different among devices
- Should modify after add/delete some elements
- Too long
How to locate an element
2. Give every element an ID
# Assume element have id 'test'
driver.find_element_by_id('test')
# or
driver.find_element_by_xpath('//*[@resource-id="test"]')
- Changing XML needs less modification
- Can exactly locate one element
- Need to change code of testing App
How to locate an element
3. Use Relative XPATH
# return element whose text is 'Welcome'
xpath = '(//*[@content-desc="hybrid-base"]/android.view.View)[1]'
driver.find_element_by_xpath(xpath)
# this works well here, but is not recommended
driver.find_element_by_xpath('//*[@content-desc="Welcome"]')
# notes:
# first index in W3C xpath syntax is 1
# compare these two xpaths
xpath1 = '//*[@content-desc="hybrid-base"]/android.view.View[1]'
xpath2 = '(//*[@content-desc="hybrid-base"]/android.view.View)[1]'
- No need to add ID to every element
- Changing XML still need modification
- Some elements are difficult to design relative XPATH
How to locate an element
4. Switch to WebView context ( Only for Hybrid App )
# print all context
print(driver.contexts)
# switch to WebView context
driver.switch_to.context("WEBVIEW_1")
# assume the element has class 'loginHeader-mainText'
driver.find_element_by_class_name("loginHeader-mainText")
# switch back to Native APP context
driver.switch_to.context("NATIVE_APP")
- Can use class name instead of ID
- Can use css selector
- CSS selector is difficult to distinguish some elements
Notes when locating elements
Remeber waiting element first
# waiting element
def wait_element_by_class_name(class_name, timeout, driver):
# count time
start_time = time.monotonic()
while True:
# check for every 1 s
time.sleep(1)
try:
driver.find_element_by_class_name(class_name)
break
except Exception:
# if timeout, throw exception and exit
if time.monotonic() - start_time > timeout:
raise Exception("Timeout when finding element: " + class_name)
continue
return driver.find_element_by_class_name(class_name)
When you use find_element_by_class_name when XPATH is not exist ( page is still loading ), it will throw exception.
Waiting for other conditions
Waiting for clickable:
# waiting element to be clickable
def wait_clickable_by_class_name(class_name, timeout, driver):
start_time = time.monotonic()
# wait element is avaliable
while True:
time.sleep(1)
try:
driver.find_element_by_class_name(class_name)
break
except Exception:
if time.monotonic() - start_time > timeout:
raise Exception("Timeout when finding element: " + class_name)
continue
# wait element to be clickable
while True:
time.sleep(1)
try:
# EC.element_to_be_clickable throw exception when element is not clickable
EC.element_to_be_clickable((By.CLASS_NAME, class_name))
break
except Exception:
if time.monotonic() - start_time > timeout:
raise Exception("Timeout when waiting element to be clickable: " + class_name)
continue
return driver.find_element_by_class_name(class_name)
Do not forget to wait for element first, otherwise you may catch an exception.
Waiting for other conditions
Waiting for staleness:
# waiting element to be dropped from XML
def wait_element_staleness_by_class_webview(class_name, timeout, driver):
element = None
# make sure that the element is in XML, otherwise stop waiting
try:
element = driver.find_element_by_class_name(class_name)
except Exception as e:
print(e)
return
finally:
if element is None:
return
# `wait` will wait until EC.staleness_of return true
wait = WebDriverWait(driver, timeout)
wait.until(EC.staleness_of(element))
This is a little different from waiting for clickable. You should check if element has been dropped from XML.
Use Appium unicode keyboard
Add desired_capability:
'unicodeKeyboard': 'true'
'resetKeyboard': 'true'
Use Appium unicode keyboard can avoid exception when calling hide_keyboard after keyboard displaying.
When testing in real devices, sogou keyboard can also cause input mistakes.
Automatic permissions agreement
Add desired_capability:
'autoGrantPermissions': 'true'
Automatic permissions agreement avoid manual clicking to agree permissions when first installing App.
Touch Actions
Zoom:
action1 = TouchAction()
action2 = TouchAction()
action1.long_press(x=centerX, y=centerY)\
.move_to(x=centerX + 50, y=centerY + 50)\
.wait(100).release()
action2.long_press(x=centerX, y=centerY)\
.move_to(x=centerX - 50, y=centerY - 50)\
.wait(100).release()
m_action = MultiAction(driver, element)
m_action.add(action1, action2)
m_action.perform()
wait(100) is used to control moving time, if it is too short, zoom operation will have no effect.
Touch Actions
Notes:
If touch actions have no effect, check source code of page and comfirm attributes of the element. These settings will forbid touch actions:
- CSS attribute touch-action of the element is none
- XML attribute scrollable of the element is false
- ...
Parallel testing
Create Appium Server Instance for every emulator:
appium -p $PORT -bp $BOOTSTRAP_PORT --chromedriver-port $CHROMEDRIVER_PORT -U $UDID
Every instance should have different
- port
- bootstrap port
- chromedriver port ( Hybrid App )
- udid
Parallel testing
Different desired_capability for devices:
'udid': 'emulator-xxxx',
'systemPort': 'xxxx'
udid and systemPort is necessary. systemPort should be different among devices and values from 8200 to 8299. If testing Hybrid App, matched chromedriver is needed.
Every device should be connected to the server set to its udid
E2E Test Framework
Pytest & Python unittest
It help to execute our testcases and print result or write log to files.
Use setUp/tearDown of unittest.TestCase to set up and close connection to Appium Server.
Run command to execute testing:
pytest test
pytest test/testcase1.py
Pytest & Python unittest
Note that execution order in a python file is not the order that you define testcases.
- Do not define testcase that depends on last defined testcase.
- Related testing functions should be execute in one testcase.
Mocha
Mocha is a JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun.
Recommend working with package Power Assert
For running on Node.js, environment is different from that in browser, which is a trouble in Hybrid App testing.
Jest
Different from Mocha, you can select two environment in Jest: jsdom and node.
Node environment is a node like environment while jsdom environment providing a browser-like environment.
So when testing React Apps or Vue Apps, it is a good choice.
Extra
PhantomJS VS Puppeteer
Syntax Support
const puppeteer = require('puppeteer')
(async () => {
const browser = await puppeteer.launch()
let page = await browser.newPage()
await page.goto('https://cn.bing.com/search?q=test', {
waitUntil: 'domcontentloaded'
})
let searchElements = await page.$$eval('ol#b_results > li.b_algo > h2 > a', nodes => nodes.map(e => e.innerHTML))
await browser.close()
})()
Codes in puppeteer:
PhantomJS VS Puppeteer
Syntax Support
var page = require('webpage').create()
page.open('https://cn.bing.com/search?q=test', function (status) {
if (status === 'success') {
var searchResults = page.evaluate(function () {
var titles = document.querySelectorAll('ol#b_results > li.b_algo > h2 > a')
var titleTexts = []
for(var i = 0; i < titles.length; i += 1){
titleTexts.push(titles[i].innerHTML)
}
return titleTexts
})
}
phantom.exit()
})
Codes in phantomJS:
PhantomJS VS Puppeteer
Debug
Puppeteer can switch to common mode:
const browser = await puppeteer.launch({headless: false})
or use slow mode:
const browser = await puppeteer.launch({
headless: false,
slowMo: 250 // slow down by 250ms
})
PhantomJS VS Puppeteer
Debug
or capture console output:
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
await page.evaluate(() => console.log(`url is ${location.href}`));
PhantomJS VS Puppeteer
Debug
PhantomJS can debug with chrome:
phantomjs --remote-debugger-port=9000 test.js
then open chrome
goto http://localhost:9000
choose link file://test.js
enter Web Inspector Interface
input in console and execute:
__run()
PhantomJS VS Puppeteer
Debug
or you can use autorun:
phantomjs --remote-debugger-port=9000 --remote-debugger-autorun=yes test.js
your test.js will be automatically executed
or capture console output:
var webPage = require('webpage')
var page = webPage.create()
page.onConsoleMessage = function (msg, lineNum, sourceId) {
console.log('CONSOLE: ' + msg + ' (from line #' + lineNum + ' in "' + sourceId + '")')
}
E2E Test Framework
By Haili Zhang
E2E Test Framework
- 770