We’ve been busy making our mobile beta testing solution better not only for mobile developers but also for SDETs who are looking for innovative ways to make their mobile test automation awesome. The challenge is how to make Appium scripts talk to the TestFairy SDK. All of the example code in this post is available as open source, so you can copy and adapt it to your needs.
We’ll be using WebdriverIO with JavaScript but all examples can be ported to other clients and languages with little to no change. The only requirement in our drivers is the ability to invoke mobile: startService via execute() which is already available on the latest UIAutomator2 drivers. Our test bed app is also included in the repo.
Our solution is based on the principle that Appium can start Android services by constructing intents over adb. These intents can hold extra arguments for services to interpret. We’ve created a simple service to parse incoming intent data to decide which TestFairy SDK method to invoke.
Every time Appium starts our service, the service reads the intent, invokes the TestFairy SDK and stops itself once the work is done. The service we launch will not interrupt any of the running activities, giving us the ability to use it without change across various types of apps including video games, social networking, and camera apps.
Here are the steps you need to take:
Copy the “Appium calls TestFairy” file into your Android app project.
Add this line to your app manifest:
<service android:exported="true" android:name=".TestFairyService" />
Add these helpers to your Appium script. As an example, we provide begin(), stop() and addEvent() but you can easily copy one of these to add an invocation for other methods as you need. Make sure you update TestFairyService to parse your newly added invocations. Update these functions with your package name by replacing: com.example.appiumcallstestfairy/.TestFairyService with com.your.package.name/.TestFairyService
1/**2* Starts recording a TestFaiy session.3*4* @param client wdio client5* @param appToken TestFairy app token can be found at https://app.testfairy.com/settings6*/7async function begin(client, appToken) {8client.startActivity()9const args = Buffer.from(JSON.stringify(['begin', appToken])).toString('base64');10await client.execute('mobile: startService', {11intent: '--es "args" "' + args + '" com.example.appiumcallstestfairy/.TestFairyService',12});13}14/**15* Sends a string event to currently recorded session. It will show up in the session timeline.16* Multiple sessions that has the same event can be searched at https://app.testfairy.com/sessions17*18* @param client wdio client19* @param event Some string value to represent a significant event happening during tests20*/21async function addEvent(client, event) {22const args = Buffer.from(JSON.stringify(['addEvent', event])).toString('base64');23await client.execute('mobile: startService', {24intent: '--es "args" "' + args + '" com.example.appiumcallstestfairy/.TestFairyService',25});26}27/**28* Stops recording a TestFairy session.29*30* @param client wdio client31*/32async function stop(client) {33const args = Buffer.from(JSON.stringify(['stop'])).toString('base64');34await client.execute('mobile: startService', {35intent: '--es "args" "' + args + '" com.example.appiumcallstestfairy/.TestFairyService',36});37}38
Then, invoke TestFairy methods wherever you need them.
1// Test suite2describe('Create TestFairy session', function () {3let client;4before(async function () {5client = await webdriverio.remote(androidOptions);6});7it('should create and destroy a session', async function () {8const res = await client.status();9assert.isObject(res.build);10const current_package = await client.getCurrentPackage();11assert.equal(current_package, 'com.example.appiumcallstestfairy');12// Start a session13console.log("Starting a TestFairy session");14await begin(client, "SDK-XXXXXXX"); // TestFairy app token can be found at https://app.testfairy.com/settings15// Make your assertions16// Mark significant points in time during the session to be able to use them later in your TestFairy dashboard for search or preview17await addEvent(client, "Initial assertions passed, this will show up in session timeline");18// Make your assertions19// Stop session20await stop(client);21console.log("Ending TestFairy session");22const delete_session = await client.deleteSession();23assert.isNull(delete_session);24});25});26
It is a small service which parses the bundled Intent extra. For simplicity, we pass all arguments as a single string value, json encoded in base64.
1@Override2public int onStartCommand(Intent intent, int flags, int startId) {3Bundle extras = intent.getExtras();4if (extras != null && extras.containsKey("args")) {5try {6final String args = extras.getString("args");7final JSONArray argsArray = new JSONArray(new String(Base64.decode(args, Base64.DEFAULT), StandardCharsets.UTF_8));8switch (argsArray.getString(0)) {9case "begin":10TestFairy.begin(getApplicationContext(), argsArray.getString(1));11break;12case "addEvent":13TestFairy.addEvent(argsArray.getString(1));14break;15case "stop":16TestFairy.stop();17break;18default:19break;20}21} catch (JSONException t) {22Log.w("TestFairyService", "Can't invoke TestFairy", t);23}24}25stopSelf();26return super.onStartCommand(intent, flags, startId);27}28
Once the call is done, we stop the service so that it does not consume more resources than necessary.
Here you can find the complete example, including the test app. Let us know if we can improve it further. Until then, stay safe and enjoy hacking!
This article was originally published in August 2021 and has been updated in March 2023.