E2E tests for Ionic using Appium (or any other Hybrid app)
Ionic is a popular framework in the world of hybrid app development. Sadly, it is well known that app testing isn’t really a major thought given by the framework designers. I believe it makes it a really bad choice for anyone who wants to write a production app using Ionic. Any good framework should encourage and facilitate testing.
While building production apps, we at LeanAgri insist at having tests to maintain high standards in the app. We have currently taken a call of not writing Unit tests and focusing only on E2E tests for our apps. Why?
- It just seems like the fastest way to move forward without breaking things. We are rapidly changing our app and we want to confirm that our app flow doesn’t break even if it is restructured.
- I think Unit tests which “Mock the world” are really not helpful for an application. (This won’t hold true for Public APIs)
There are a couple of blog posts which explain how to run Ionic E2E tests using ionic serve
. Unfortunately, they don’t qualify as E2E tests for us since they are run on a browser, and an app with any added native functionality wouldn’t work on a browser.
Here’s the missing guide to writing a real end to end test with Ionic using Appium.
First and foremost, the acknowledgements: I would thank Microsoft guys for taking the time to write some very good documentation on WD.js which doesn’t exist on the web.
Testing mechanism
- Deploy the latest version of your Android app using ionic
- Start Appium server to connect with the Android device
- Run the tests using Protractor
The stack
- Appium with any Android device / Emulator – Read the web on how to install
- Working chromedriver for the app you are testing – You can find compatibility information here. This is really important, if you launch your tests with an incompatible version of chromedriver, tests would fail with usually arbitrary reasons.
- Testing tools: I ran the tests using Protractor and Jasmine, read below if you want to use WD.js or WebdriverIO for running the tests.
Setup Protractor config:
var SpecReporter = require('jasmine-spec-reporter').SpecReporter; const util = require('util'); const exec = util.promisify(require('child_process').exec); const spawn = require('child_process').spawn; PromiseTool = require('promise-tool'); var appiumProcess; var config = { seleniumAddress: 'http://localhost:4723/wd/hub', specs: ['./e2e/*.e2e-spec.ts'], // Spec files are stored here multiCapabilities: [{ browserName: '', // Leave as blank to run on Appium appPackage: 'io.ionic.starter', // Name of the package appActivity: '.MainActivity', // MainActivity is the activity to launch platformName: 'Android', deviceName: '11cad4be', autoAcceptAlerts: 'true', autoWebview: true // Sets to Webview to allow testing }], framework: 'jasmine', SELENIUM_PROMISE_MANAGER: false, useAllAngular2AppRoots: true, random: false, jasmineNodeOpts: { random: false, }, onPrepare: function () { jasmine.getEnv().addReporter(new SpecReporter()); }, beforeLaunch: async () => { require('ts-node').register({ project: 'e2e' }); } };
An example test login.e2e-spec.ts
. Test files should be added in e2e/
folder according to our config above.
import { browser, element, by, ElementFinder, protractor } from 'protractor'; describe('Login', () => { beforeEach(() => { var until = protractor.ExpectedConditions; browser.wait(until.presenceOf(element(by.css('#username input'))), 30000, 'Element didn\'t load till 30 seconds'); }); it('Click login button with valid credentials', async () => { const usernameInput = await element(by.css('#username input')); await usernameInput.clear(); await usernameInput.sendKeys('username'); const passwordInput = await element(by.css('#password input')); await passwordInput.clear(); await passwordInput.sendKeys('password'); await expect(element(by.css('.input .label')) .getAttribute('innerHTML')) .toContain('Username'); await element(by.css('#loginButton')).click(); await expect(element(by.css('.input .label')) .getAttribute('innerHTML')) .toContain('Logged In'); }); });
The above config and example should be self-explanatory. (Leave a comment if it isn’t and I will do my best to help you).
Running the tests
- Start appium server with the appropriate version of chromedriver according to your test device. Repeating, this is necessary
I install both of them as dependencies, so I can just run using:./node_modules/.bin/appium --chromedriver-executable ./node_modules/.bin/chromedriver
- Run the test using
protractor
Automating everything
I did a couple of further hacks to allow my team to run the tests using a single command.
- Added
pretest
to package.json which deploys the app on phone usingionic cordova run android
- Protractor starts and stops the appium server using magic of
beforeLaunch
andafterLaunch
in my protractor config. Here is my final configvar SpecReporter = require('jasmine-spec-reporter').SpecReporter; const util = require('util'); const exec = util.promisify(require('child_process').exec); const spawn = require('child_process').spawn; PromiseTool = require('promise-tool'); var appiumProcess; var config = { seleniumAddress: 'http://localhost:4723/wd/hub', specs: ['./e2e/*.e2e-spec.ts'], multiCapabilities: [{ browserName: '', appPackage: 'io.ionic.starter', appActivity: '.MainActivity', platformName: 'Android', deviceName: '11cad4be', autoAcceptAlerts: 'true', autoWebview: true }], framework: 'jasmine', SELENIUM_PROMISE_MANAGER: false, useAllAngular2AppRoots: true, random: false, jasmineNodeOpts: { random: false, }, onPrepare: function () { jasmine.getEnv().addReporter(new SpecReporter()); }, beforeLaunch: async () => { require('ts-node').register({ project: 'e2e' }); await launch_appium(); }, afterLaunch: async (code) => { // Stop appium server appiumProcess.kill('SIGINT'); } }; exports.config = config; function launch_appium() { const appium = spawn(process.env.PWD + '/node_modules/.bin/appium', ['--chromedriver-executable', process.env.PWD + '/node_modules/.bin/chromedriver']); appium.stdout.on('data', data => { // console.log(`stdout: ${data}`); }) appium.stderr.on('data', data => { console.log(`ERROR: ${data}`); }) appiumProcess = appium; // Timeout to wait for appium to startup return PromiseTool.setTimeout(10000); }
Brownie points
Other ways of testing if you don’t want to use protractor based setup.
- Using WebdriverIO for testing hybrid apps with Appium
/// Using WebdriverIO var webdriverio = require('webdriverio'); var client = webdriverio.remote({ port: 4723, logLevel: 'verbose', desiredCapabilities: { browserName: '', app: '/home/kunal/leanagri-data-collection/platforms/android/app/build/outputs/apk/debug/app-debug.apk', platformName: 'Android', deviceName: '11cad4be', autoAcceptAlerts: 'true', automationName: 'Appium', autoWebview: true }, services: ['appium'] }); client.init() .pause(2000) .waitForExist('#username') .clearElement('#username input') .setValue('#username input', 'kunal') .click('#loginButton') .pause(2000) .end()
- Using WD.js for testing hybrid apps with Appium
var wd = require("wd"); var appDriver = wd.promiseChainRemote({ hostname: 'localhost', port: 4723, }); config = { browserName: '', app: '/home/kunal/leanagri-data-collection/platforms/android/app/build/outputs/apk/debug/app-debug.apk', platformName: 'Android', deviceName: '11cad4be', autoAcceptAlerts: 'true', automationName: 'Appium', autoWebview: true }; /// Method 1 WD appDriver.init(config) .sleep(3000) .waitForElementByCssSelector('#username input') .elementByCssSelector('#username input') .sendKeys('qeruty') .elementById('loginButton') .click() .sleep(4000) .fin(function() { return appDriver.quit(); }) .done(); /// Method 2 WD (Allows debugging) appDriver.init(config, function () { console.log("initialized"); appDriver.sleep(3000, function () { console.log("Sleeping completed"); appDriver.elementByCssSelector('#username input', function (err, element) { console.log("Found element"); element.sendKeys('', function (err) { console.log("Sent keys"); console.log(err); appDriver.elementByCssSelector('#loginButton', function(err, element) { element.click(function() { appDriver.sleep(3000, function () { appDriver.quit(); }); }); }) }); }); });
One question that comes to my mind is why should I use protractor and Appium to test Ionic based apps ? Won’t Java+ Appium be sufficient to test ionic based apps ?
That’s a good point. Actually, it might be even better than writing tests in Protractor given the support in Device farms such as AWS Device farm, Firebase test lab etc.
On the other hand, tools which are comfortably in use by your team are important. Given that Angular developers write end to end tests regularly in Protractor and you don’t have any dependency on a Device farm provider, it does make a lot more sense to use Protractor.
i got stuck here bec i am new to this ,
1) how can i configure my ionic app with appium