Tuesday, November 27, 2012

AndroidViewClient: UiAutomator support

AndroidViewClient v2.3.1 has been released recently providing UiAutomator compatibility, when supported by the device or emulator. UiAutomator is supported since Android API 16.

This is a great improvement over previous version in two different aspects:

  • it can now be run on non-rooted devices not demanding application modification (as previous version required to use LocalViewServer)
  • the change in the backend now frees the client from port redirection, as ViewServer requests, and at the same time the performance of dumping the View tree is greatly improved
 As an introduction of this new release we will be using a simple example that demonstrates some of the new features. This example us based on the demo application AndroidSampleUi.apk.

This example demonstrates:

  • automatic device connection, handling command line parameters if present
  • automatic View tree dump
  • finding Views using regular expressions or text
  • touching found Views
As a precondition to run this example, install and run AndroidSampleUI.


Zoom buttons let you increase or decrease the margins and consequently move the toggle buttons to demonstrate that they will be found whatever their coordinates are.

#! /usr/bin/env monkeyrunner
'''
Copyright (C) 2012  Diego Torres Milano
Created on Aug 31, 2012

@author: diego
'''


import re
import sys
import os

# This must be imported before MonkeyRunner and MonkeyDevice,
# otherwise the import fails.
# PyDev sets PYTHONPATH, use it
try:
    for p in os.environ['PYTHONPATH'].split(':'):
       if not p in sys.path:
          sys.path.append(p)
except:
    pass

try:
    sys.path.append(os.path.join(os.environ['ANDROID_VIEW_CLIENT_HOME'], 'src'))
except:
    pass
from com.dtmilano.android.viewclient import ViewClient, ViewNotFoundException

vc = ViewClient(*ViewClient.connectToDeviceOrExit())

# Find the 3 toggle buttons, because the first 2 change their text if they are selected
# we use a regex to find them.
# Once found, we touch them changing their state
for t in [re.compile('Button 1 .*'), re.compile('Button 2 .*'), 'Button with ID']:
    try:
        vc.findViewWithTextOrRaise(t).touch()
    except ViewNotFoundException:
        print >>sys.stderr, "Couldn't find button with text=", t

Once this script is run, ViewClient will find a device, which can be specified using its serial number in the command line invoking the script, connects to, automatically dump the tree and then use regular expressions to find two of the ToggleButtons because we couldn't use a fixed text because it changes when the button is clicked. If, for some reason the Button is not found, perhaps because it was move outside the screen using Zoom buttons, a message is printed.

This screenshot show the state of the Buttons after the script has run.



More articles and examples will be coming soon, but I didn't want to miss the opportunity to introduce this new version. One of the most remarkably advantages over plain UiAutomator is the  simplification of the script or test creation and the expressiveness gain of using Python instead of Java.

dump.py

dump.py is also present in AndroidViewClient examples. It now supports several command line options now

usage: dump.py [-u|--uniqueId] [-x|--position] [-d|--content-description] [serialno]

so we can use it to verify the content of the screen. If everything went well running

dump.py --content-description

will show the View tree including also the content descriptions, as given by the following dump


android.widget.FrameLayout id/no_id/1  
   android.widget.LinearLayout id/no_id/2  
      android.widget.FrameLayout id/no_id/3  
         android.view.View id/no_id/4  
            android.widget.FrameLayout id/no_id/5  
               android.widget.ImageView id/no_id/6  
            android.widget.LinearLayout id/no_id/7  
               android.widget.LinearLayout id/no_id/8  
                  android.widget.TextView id/no_id/9 Sample UI v2.0 
      android.widget.FrameLayout id/no_id/10  
         android.widget.RelativeLayout id/no_id/11  
            android.widget.Button id/no_id/12 Show Dialog show_dialog
            android.widget.LinearLayout id/no_id/13  
               android.widget.TextView id/no_id/14  
               android.widget.ToggleButton id/no_id/15 Button 1 OFF button_1
               android.widget.TextView id/no_id/16 v=(75.0,82.0) lw=(115,272) ls=(115,272) wxh=(290,72) margin=(40,80) button_1_info
               android.widget.ToggleButton id/no_id/17 Button 2 OFF button_2
               android.widget.TextView id/no_id/18 v=(75.0,273.0) lw=(115,463) ls=(115,463) wxh=(290,72) margin=(40,80) button_2_info
               android.widget.ToggleButton id/no_id/19 Button with ID button_with_id
            android.widget.ZoomControls id/no_id/20  zoom
               android.widget.ZoomButton id/no_id/21  
               android.widget.ZoomButton id/no_id/22  


UPDATE:
Changed linked version to AndroidViewClient 2.3.1 as some latest commits were not in version 2.3 as found by Durairaj.

Friday, November 16, 2012

AndroidViewClient: Getting Browser's HTML page source

Standard Android Browser does not provide an option in its menu to view the HTML page source. Some workarounds like installing apps and then using Share page from Browser's menu has been described and also the use of Javascript and JQuery to add to a page has been detailed, but we are hungry for more. Needless to say, all these methods involve some manual step so I felt the need to find a completely automatic way of doing it.

Of course, to do it I would resort to our old pal AndroidViewClient. This is a very interesting example of its use because it lies far from testing and application or UI.

And so, without further ado, let me introduce you to the code...


#! /usr/bin/env monkeyrunner
'''
Copyright (C) 2012  Diego Torres Milano
Created on Oct 12, 2012

@author: diego
'''


import re
import sys
import os

# This must be imported before MonkeyRunner and MonkeyDevice,
# otherwise the import fails.
# PyDev sets PYTHONPATH, use it
try:
    for p in os.environ['PYTHONPATH'].split(':'):
        if not p in sys.path:
            sys.path.append(p)
except:
    pass

try:
    sys.path.append(os.path.join(os.environ['ANDROID_VIEW_CLIENT_HOME'], 'src'))
except:
    pass

from com.dtmilano.android.viewclient import ViewClient

from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice

VPS = "javascript:alert(document.getElementsByTagName('html')[0].innerHTML);"
PACKAGE = 'com.android.browser'
ACTIVITY = '.BrowserActivity'
COMPONENT = PACKAGE + "/" + ACTIVITY
URI = 'http://dtmilano.blogspot.com'


device, serialno = ViewClient.connectToDeviceOrExit()

device.startActivity(component=COMPONENT, uri=URI)
MonkeyRunner.sleep(3)

vc = ViewClient(device=device, serialno=serialno)

device.drag((240, 180), (240, 420), 10, 10)

url = vc.findViewByIdOrRaise('id/url')
url.touch()
MonkeyRunner.sleep(1)

device.press('KEYCODE_DEL', MonkeyDevice.DOWN_AND_UP)
for c in VPS:
    device.type(c)
device.press('KEYCODE_ENTER', MonkeyDevice.DOWN_AND_UP)
MonkeyRunner.sleep(3)

vc.dump()
print vc.findViewByIdOrRaise('id/message').getText().replace('\\n', "\n")

device.press('KEYCODE_BACK', MonkeyDevice.DOWN_AND_UP)



And now a brief explanation of the most important pieces of this script.

  1. Shebang, you know, to invoke monkeyrunner as the interpreter. I don't have to tell you more (if you are a poor Windows user you may have to invoke monkeyrunner from command line, I feel sad for you)
  2. Some comments and imports
  3. Read PYTHONPATH just in case you are using Eclipse and Pydev (this has been explained in this post)
  4. Then use ANDROID_VIEW_CLIENT_HOME environment variable to find AndroidViewClient in your system
  5. Some constants defined. VPS is the actual javascript to obtain the page source
  6. The standard way of connecting to the device or emulator in AndroidViewClient. This handles errors and timeout automatically solving many problems you find with bare monkeyrunner
  7. We start Browser
  8. Drag a bit to make the URL visible in case the page has scrolled
  9. Next, we find the View with ID id/url, which you know, contains the URL
  10. We touch to focus
  11. And type the javascript in VPS followed by ENTER
  12. By that time the alert dialog should be on screen so we take a new dump
  13. Now we find the View with ID id/message which contains the HTML and print it
  14. Finally, we press BACK to dismiss it
I hope you have enjoyed it as much as I did and this help you find new ways of using AndroidViewClient.

P.S. This script will be part of AndroidViewClient source code distribution examples

Monday, October 15, 2012

AndroidViewClient @ Google+ pages


Until recently, the number of comments related to AndroidViewClient in this blog were not very high and everything was smooth. But, as the number of comments increased and given the non-hierarchical nature of blogspot comments it turned to be a huge mess and it became almost impossible to match a comment, usually containing a question, with its reply.

So, here I am, creating AndroidViewClient's Google+ page where we can easily follow up every comment, or at least this is my hope. The format for these posts is very restrictive, but I think it might be enough.
Time will tell.

Bookmark AndroidViewClient's Google+ pagehttps://plus.google.com/111731764904697052166


Wednesday, September 19, 2012

IXONOS sponsored AndroidViewClient improvements

Thanks to IXONOS and its commitment with the Open Source community AndroidViewClient has reached a new level of maturity. IXONOS has sponsored the improvement of some core AndroidViewClient features broaden its usage and allowing it to be used as a test workhorse of many other projects.

Ari Manninen, from IXONOS said:

"AndroidViewClient is a very valuable tool for creating device independent MonkeyRunner scripts. It has greatly enhanced MonkeyRunner testing in our customer projects."

Tuesday, September 11, 2012

monkeyrunner: importing from PYTHONPATH

In previous post we analyzed what is needed to develop, run and debug monkeyrunner scripts using Eclipse and PyDev.


#! /usr/bin/env monkeyrunner
'''
Created on Sep 10, 2012

@author: diego
'''

import re
import sys
import os
import java

# This must be imported before MonkeyRunner and MonkeyDevice,
# otherwise the import fails.
# PyDev sets PYTHONPATH, use it
try:
    for p in os.environ['PYTHONPATH'].split(':'):
       if not p in sys.path:
          sys.path.append(p)
except:
    pass

try:
    sys.path.append(os.path.join(os.environ['ANDROID_VIEW_CLIENT_HOME'], 'src'))
except:
    pass

from com.dtmilano.android.viewclient import ViewClient, View
from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice

# usage: script [serialno]
serialno = sys.argv[1] if len(sys.argv) > 1 else 'emulator-5554'
device = MonkeyRunner.waitForConnection(30, serialno)
try:
    device.wake()
except java.lang.NullPointerException, e:
    print "ERROR: Couldn't connect to %s: %s" % (serialno, e)

These are the lines you should add to every monkeyrunner script. Here you are a brief explanation of the snippet.

  1. The shebang line to invoke monkeyrunner  interpreter if you are using Linux or Mac OS X. Unfortunately this is not available on Windows. Eclipse does not use this line but is needed if you want to simplify the way you are running the scripts from the command line.
  2. Some standard imports
  3. PyDev uses PYTHONPATH while monkeyrunner ignores it. This snippet adds the components present in PYTHONPATH to sys.path and makes them visible to monkeyrunner.
  4. Following, we need to locate AndroidViewClient which you should have added to the environment. This can be also added in Eclipse in Run Configurations -> Environment.
    ANDROID_VIEW_CLIENT_HOME should point to your AndroidViewClient installation to the parent folder of src. That is, if you have downloaded AndroidViewClient in /opt/AndroidViewClient and kept the same structure as the distribution, you should set ANDROID_VIEW_CLIENT_HOME=/opt/AndroidViewClient/AndroidViewClient
  5. The imports, which will now succeed because sys.path contains the right components
  6. Gets the device's serial number from the command line or default to emulator-5554.
  7. Connect to the device
  8. Check if the connection was successful. Because MonkeyRunner.waitForConnection() returns a MonkeyDevice even when the connection fails we need to go to this extra step to verify it.



Wednesday, August 29, 2012

monkeyrunner: setting Eclipse PyDev interpreter

There have been some popular posts in this blog describing the use of Eclipse, PyDev and monkeyrunner. However, I have received a lot of questions regarding the definition of the interpreter and perhaps it requires a little deep explanation.

Even though monkeyrunner is a Jython interpreter, to successfully use it from PyDev you have to identify it as plain Python.

This screenshot shows the definition of the interpreter which coincidentally, we called monkeyrunner.


Following these steps you would have no problems using monkeyrunner from PyDev.
Hope this helps.

Friday, August 17, 2012

monkeyrunner: detecting the OS

Sometimes you monkeyrunner script should know the Operating System it is running on. In the big majority of the cases you don't have to worry if you are running on Linux or Mac OS X, but things are not so smooth on Windows.

I'll give you an example. I've received some bug reports about AndroidViewClient not being able to find adb. AndroidViewClient tries to be clever and not to invoke adb if it's going to fail because it's not found or it's not executable. To determine this, it is using:


        if not os.access(adb, os.X_OK):
            raise Exception('adb="%s" is not executable' % adb)

the trick here is that for Windows platforms adb should include the trailing .exe.
Then the problem is to determine the OS the script is running on.

There are several ways of determining the OS in python and jython. Let's see what are the results using monkeyrunner

Command Linux Mac OS X Windows
os.getenv('os') None None Windows_NT
os.name java java java
platform.system() Java Java Java
sys.platform java1.6.0_26 java1.6.0_33 java1.7.0_05
java.lang.System.getProperty('os.name') Linux Mac OS X Windows XP

From the previous table we can determine that the best way of obtaining the OS from a monkeyrunner script is

     java.lang.System.getProperty('os.name')

I hope this helps you

Wednesday, August 01, 2012

Introduction to Android Testing @ OSCON 2012

It was great having presented my tutorial at OSCON 2012. Definitely is one of the best organized conferences in the world and everything was as expected and even better in most cases. It was a pleasure and an honour for me to be there.
I really want to thank you all.

For those who missed it, here are the slides that are also available at the OSCON 2012 web site: Introduction to Android Testing Presentation.

Monday, June 04, 2012

monkeyrunner: Q&A

Q: 
Hi,
Thanks for the response.
To get more specific for what I was looking at is, to use getCallState()getDataActivity() etc.


[Reference: http://developer.android.com/reference/android/telephony/TelephonyManager.html] is Monkeyrunner scripts.


Please let me know if this is a possible idea,




(This question was posted as a comment to monkeyrunner:  visual image comparison)

A: 
Taking the right approach this is pretty simple and straightforward. If you have been followed the posts in this blog you may have noticed some time ago we introduced AndroidViewClient in monkeyrunner: interacting with the Views. Well, we can use exactly the same technique to invoke Android services and get the results, but in this case we will be invoking the phone service instead.

#! /usr/bin/env monkeyrunner
'''
Created on Jun 2, 2012

Take a look at:
    <android>/frameworks/base/telephony/java/com/android/internal/telephony/ITelephony.aidl
    <android>/out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/telephony/ja
va/com/android/internal/telephony/ITelephony.java
    <android>/java/android/telephony/TelephonyManager.java

@author: diego
'''

import sys
import re

from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice

DEBUG = True
android_os_IBinder_FIRST_CALL_TRANSACTION = 1
TRANSACTION_getCallState = android_os_IBinder_FIRST_CALL_TRANSACTION + 28
TRANSACTION_getDataActivity = android_os_IBinder_FIRST_CALL_TRANSACTION + 29;

def telephonyManager(device, transaction):
    return serviceResponse(device.shell('service call phone %d' % transaction))

def serviceResponse(response):
    m = re.match("Result: Parcel\((\d+) (\d+)   '........'\)\r\n", response)
    if m:
        return int(m.group(2))
    return -1

def main():
    device = MonkeyRunner.waitForConnection(60)

    print "call state: %d" % telephonyManager(device, TRANSACTION_getCallState)
    print "data activity: %d" % telephonyManager(device, TRANSACTION_getDataActivity)

if __name__ == '__main__':
    main()

Obviously, this can be extended to support other transactions or to interpret the results results in other ways.
Also, this same technique can be used to invoke other services.
Hope this helps.



Saturday, May 12, 2012

AndroidViewClient: Q&A

Q: Hi Diego, thanks for your sharing knowledge.
Now I have a question about how to implement a method like touchByText(self, text)instead of touching the Views by (x,y).


I just want to simply use text instead of android id.
Thank you.


(edited for clarity)

A: This is an interesting question that was posted as a comment to monkeyrunner: interacting with the Views and made me think about the possibility of including this functionality in AndroidViewClient.


After all, one of the most serious limitations of plain monkeyrunner is the need of the screen coordinates in MonkeyDevice.touch(integer x, integer y, integer type).
We have also analyzed here the use of the undocumented EasyMonkeyDevice in monkeyrunner: testing views properties where we described the current shortcomings.


So, fortunately, implementing this feature in AndroidViewClient was not so difficult and it's now available if you download the latest source code. To demonstrate it, we will be using a very simple Activity with 5 ToggleButtons named One, Two, Three, Four and Five.
Then, we will be using a monkeyrunner script using AndroidViewClient to find the buttons and touching them. After the script runs we will be able to see the five buttons in their On state.




The script that will toggle every button on is as follows:

#! /usr/bin/env monkeyrunner
'''
Copyright (C) 2012  Diego Torres Milano
Created on May 5, 2012
  
@author: diego
'''

import sys
import os
import time

# this must be imported before MonkeyRunner and MonkeyDevice,
# otherwise the import fails
try:
    ANDROID_VIEW_CLIENT_HOME = os.environ['ANDROID_VIEW_CLIENT_HOME']
except KeyError:
    print >>sys.stderr, "%s: ERROR: ANDROID_VIEW_CLIENT_HOME not set in environment" % __file__
    sys.exit(1)
sys.path.append(ANDROID_VIEW_CLIENT_HOME + '/src')
from com.dtmilano.android.viewclient import ViewClient

from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice

device = MonkeyRunner.waitForConnection(60, "emulator-5554")
if not device:
   raise Exception('Cannot connect to device')

MonkeyRunner.sleep(5)

vc = ViewClient(device)
vc.dump()

for bt in [ 'One', 'Two', 'Three', 'Four', 'Five' ]:
    b = vc.findViewWithAttribute('text:mText', bt)
    if b:
        (x, y) = b.getXY()
        print >>sys.stderr, "clicking b%s @ (%d,%d) ..." % (bt, x, y)
        b.touch()
    else:
        print >>sys.stderr, "b%s not found" % bt
    time.sleep(7)

print >>sys.stderr, "bye"


Once you run the script you will see how the state of the buttons is gradually changed.
I hope this example helps you getting started with AndroidViewClient.

Wednesday, April 25, 2012

All you wanted to know about Android Testing, but were too affraid to ask

I will be presenting the Introduction to Android Testing tutorial at OSCON 2012. It will give you an overview of current methodologies and tools available on Android. This tutorial will also introduce Test Driven Development, Behaviour Driven Development and Continuous Integration, techniques that every serious development project should at least consider.

If you are planning to attend you can use the following 20% off discount code while registering: OS12FOS.
Hope to see you there.

Tuesday, April 10, 2012

android: testing library projects

The latest Android SDK Tools (Revision 17 or greater) features several fixes related with library projects and the way the R class is now generated, and the way custom attributes for custom views are handled. This opens a greater number of possibilities using library projects and sooner or later you will be facing the need of testing the library.


Following there is a quick reference of how to do it for a sample library project called AndroidLibrary and its corresponding test project AndroidLibraryTest.

Create library project

Set properties to indicate it is a Library

Create test project and set the properties to reference the library

Set the Target package to this same test project

Run the tests

Wednesday, March 21, 2012

Selecting the adb device

How many times have your received an error message like this


error: more than one device and emulator


when you run the adb command ?


I guess many, if like me, you normally use several devices an emulators. When this happens, you should obtain the serial number using


$ adb devices


then cut & paste the desired serial number into the command line and run the desired command again. Imagine how much time is wasted if this occurs tens or even hundreds of times during your day.


Hopefully, Linux and Mac OSX (or perhaps Cygwin if you are using Windows) give you the power to change what you don't like, so the following scripts will transparently allow you to select a device from the list when there's more than one a it wasn't specified in the command line.


android-select-device
This script, which is called android-select-device, is the responsibly of prompting the user for the selection of the device.


#! /bin/bash
# selects an android device

PROGNAME=$(basename $0)
UNAME=$(uname)
DEVICE_OPT=
for opt in "$@"
do
   case "$opt" in
      -d|-e|-s)
         DEVICE_OPT=$opt
         ;;
   esac
done
[ -n "$DEVICE_OPT" ] && exit 0
DEV=$(adb devices 2>&1 | tail -n +2 | sed '/^$/d')
if [ -z "$DEV" ]
then
   echo "$PROGNAME: ERROR: There's no connected devices." >&2
   exit 1
fi
N=$(echo "$DEV" | wc -l | sed 's/ //g')

case $N in
1)
   # only one device detected
   D=$DEV
   ;;

*)
   # more than one device detected
   OLDIFS=$IFS
   IFS="
"
   PS3="Select the device to use, <Q> to quit: "
   select D in $DEV
   do
      [ "$REPLY" = 'q' -o "$REPLY" = 'Q' ] && exit 2
      [ -n "$D" ] && break
   done

   IFS=$OLDIFS
   ;;
esac

if [ -z "$D" ]
then
   echo "$PROGNAME: ERROR: target device coulnd't be determined" >&2
   exit 1
fi

# this didn't work on Darwin
# echo "-s ${D%% *}"
echo "-s $(echo ${D} | sed 's/ .*$//')"

my-adb
This is the other component of our solution. This script, which we are calling my-adb will be the adb replacement which ultimately invokes the real adb.

#! /bin/bash
# This command can be used as an alias for adb and it will prompt for the
# device selection if needed
#   alias adb=my-adb

set +x
PROGNAME=$(basename $0)
ADB=$(which adb)
if [ -z "$ADB" ]
then
   echo "$PROGNAME: ERROR: cannot found adb"
   exit 1
fi

set -e
if [ $# == 0 ]
then
   # no arguments
   exec $ADB
elif [ "$1" == 'devices' ]
then
   # adb devices should not accept -s, -e or -d
   exec $ADB devices
else
   # because of the set -e, if selecting the device fails it exits
   S=$(android-select-device "$@")
   exec $ADB $S "$@"
fi


final step
The final step is to put this solution in place. To achieve this we need a way of replacing a normal adb command with the modified version. The alias shell's internal command is the best way of getting this done (you can add it to ~/.bash_aliases):


$ alias adb=my-adb


providing that the scripts are in your PATH and execute permission was granted.
So now, every time you type adb without specifying the target device, android-select-device will prompt you for the selection:



$ adb shell
1) 02783201431feeee device           3) emulator-5554
2) 3832380FA5F30000 device           4) emulator-5556
Select the device to use, <Q> to quit: 1
$

Hope this saves you some time.

Friday, March 16, 2012

Eclipse: working monkeyrunner configuration

This post is intended to help you if you have problems running monkeyrunner from Eclipse.
There are tons of messages floating around describing a variety of problems. It seems that the most problematic platform in this respect is Microsoft Windows, and Linux or Mac OSX are both much less tricky.


Using Android monkeyrunner from Eclipse is one of the all-time most popular post in this blog. Clearly, this indicates that the setup is not as straightforward as it should be, so I decided to post a detailed configuration that has been tested and is the one I mostly use to develop tools like AndroidViewClient, which has been described in latests posts like monkeyrunner: interacting with the Views.


After this brief introduction we are ready to start, firstly my Eclipse Helios configuration:

  •   Android DDMS 16.0.1.v201112150204-238534
  •   Android Development Tools 16.0.1.v201112150204-238534
  •   Android Hierarchy Viewer 16.0.1.v201112150204-238534
  •   Android Traceview 16.0.1.v201112150204-238534
  •   AspectJ Development Tools 2.1.3.e36x-20110622-1300
  •   Cross References tool (XRef) 2.1.3.e36x-20110622-1300
  •   EclEmma Java Code Coverage 2.0.1.201112281951
  •   Eclipse EGit 1.2.0.201112221803-r
  •   Eclipse IDE for Java Developers 1.3.2.20110301-1807
  •   Eclipse JGit 1.2.0.201112221803-r
  •   Eclipse Weaving Service Feature 2.1.3.e36x-20110622-1300
  •   Equinox Weaving SDK 1.0.0.v20100421-79--EVVFNFFsFc
  •   m2e - Maven Integration for Eclipse 1.0.100.20110804-1717
  •   m2e - slf4j over logback logging (Optional) 1.0.100.20110804-1717
  •   PyDev for Eclipse 2.2.0.2011062419
  •   Pydev Mylyn Integration 0.3.0


Following this configuration we will be using one of the AndroidViewClient's example: browser-open-url.py. This is showing its run configuration.

main

arguments

interpreter

refresh

environment

common





Monday, March 12, 2012

monkeyrunner: running unit tests

First of all, I hope you like the new look of the blog. Personally I think the darker background turns the posts more relevant and less distracting.


Let me know what you think.


Now, to the point. We have presented an analyzed here many monkeyrunner scripts and techniques but we haven't explicitly created some unit tests, and this is precisely what we are going to do.


As our Application Under Test (AUT) we are using the well-known Temperature Converter, which was used many times to demonstrate other concepts as well.
The source code is as usual available through github.


Additionally, we are also using the help of AndroidViewClient which is available at github too and was introduced in monkeyrunner: interacting with the Views. The central idea is to use AndroidViewClient facilities to create a unit test. Let's name this script temperature-converter-view-client.mr.

#! /usr/bin/env monkeyrunner

'''
Created on 2012-03-08

@author: diego
'''

import sys
import unittest
import subprocess
import socket
import re
from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice
import viewclient

class TemperatureConverterActivityTests(unittest.TestCase):

   def setUp(self):
      # connect to the device
      self.device = MonkeyRunner.waitForConnection(60)
      self.assertNotEqual(None, self.device)
      self.device.wake()

      # start TemperatureConverter
      self.device.startActivity(component="com.example.i2at.tc/.TemperatureConverterActivity")

      # clear the field
      for n in range(10):
         self.device.press('DEL', MonkeyDevice.DOWN_AND_UP)

      # create the client
      self.viewclient = viewclient.ViewClient(self.device)


   def tearDown(self):
      pass

   def testConversion(self):
      C = '123'
      F = '253.4'

      MonkeyRunner.sleep(1)
      self.device.type(C)
      MonkeyRunner.sleep(1)

      self.viewclient.dump()

      celsius = self.viewclient.findViewById('id/celsius')['mText']
      fahrenheit = self.viewclient.findViewById('id/fahrenheit')['mText']

      self.assertEqual(celsius, C)
      self.assertEqual(fahrenheit, F)


if __name__ == '__main__':
   unittest.main()

It is noticeable how the use of AndroidViewClient simplifies this test.


So if everything goes well, providing that your emulator or device is running and reachable, you will receive this output for the test run:

$ ./temperature-converter-view-client.mr 
.
----------------------------------------------------------------------
Ran 1 test in 8.801s

OK

Hope this helps.

Tuesday, February 07, 2012

monkeyrunner: interacting with the Views

Amazingly, this post is at the top of the stats meaning that still a lot of people are still using monkeyrunner and haven't discovered yet AndroidViewClient/culebra. Come on! It's time to evolve.



The time may come when you want your tests to interact with the Application Under Test (AUT) in a more clever way than just guessing the View's coordinates on the screen and sending the events.

Furthermore, this is sometimes not possible at all because in order to send the events you may need to obtain some View state. Let me give you an example to illustrate this and if you want you can try to solve it using monkeyrunner.

We can use Development Settings as our AUT and our intention could be to activate the Show running processes and Immediately destroy activities settings.
As we may accustom to do, we can obtain the coordinates of these Views on the screen and send the corresponding touch events using MonkeyDevice.touch() as usual. Sonner or later, maybe sooner, we will discover that because these settings are persistent we should know the state before sending the event, otherwise we will be changing its state other than just enabling this settings as is this example's intention.


We introduced a way of doing things like that in 
Automated Android testing using Sikuli using visual comparison and obtaining properties like the text in EditText's in monkeyrunner: testing views properties (which depends on a patch to chimpchat that has not yet been approved) but now we are craving for a more general approach unless we had the intention to patch chimpchat to support all of the properties in the different Views.

This approach is AndroidViewClient that you can download and install from github. Still has its rough edges but I wanted to show its functionality here and be open to comments.
AndroidViewClient adds to monkeyrunner the ability of
  • finding Views by ID, very much like you normally do in your Android Activity (using ViewClient.findViewById())
  • obtaining the value for almost any of the Views properties (using for example View.isChecked() or View.mText())
  • sending touch events to the Views by simply invoking View.touch()
The following script is a case of these abilities in action.

-->
#! /usr/bin/env monkeyrunner
'''
Copyright (C) 2012  Diego Torres Milano
Created on Feb 3, 2012

@author: diego
'''


import re
import sys
import os

# this must be imported before MonkeyRunner and MonkeyDevice,
# otherwise the import fails
try:
    ANDROID_VIEW_CLIENT_HOME = os.environ['ANDROID_VIEW_CLIENT_HOME']
except KeyError:
    print >>sys.stderr, "%s: ERROR: ANDROID_VIEW_CLIENT_HOME not set in environment" % __file__
    sys.exit(1)
sys.path.append(ANDROID_VIEW_CLIENT_HOME + '/src')
from com.dtmilano.android.viewclient import ViewClient

from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice


# Displayed com.android.development/.DevelopmentSettings: +379ms
package = 'com.android.development'
activity = '.DevelopmentSettings'
componentName = package + "/" + activity
device = MonkeyRunner.waitForConnection(60, "emulator-5554")
if not device:
   raise Exception('Cannot connect to device')

device.startActivity(component=componentName)
MonkeyRunner.sleep(5)

vc = ViewClient(device)
vc.dump()

showCpu = vc.findViewById("id/show_cpu")
showLoad = vc.findViewById("id/show_load")
alwaysFinish = vc.findViewById("id/always_finish")

if not showLoad.isChecked():
    showLoad.touch()

if not alwaysFinish.isChecked():
    alwaysFinish.touch()

if not showCpu.isChecked():
    # WARNING: Show CPU usage is de-activated as soon as it's activated, that's why it seems it
    # is never set
    showCpu.touch()

Once we run this script against a running emulator or device we will be able to see how the settings are enabled.


Moreover, if you run the script again you will see how the settings remain unchanged.
AndroidViewClient is a work in progress but it is already useful for many cases where the monkeyrunner scripts are not enough or where the complexity of achieving the desired goals is too high.

Give AndroidViewClient a try, share your comments, suggestions, patches and scripts here to help improve it.

Saturday, January 28, 2012

monkeyrunner: testing views properties

There are several questions floating around, like this one in stackoverflow, about how some view properties could be obtained from a monkeyrunner script or putting the question in more general terms:
how a test that verifies some properties can be created using monkeyrunner ?


This is a required feature if we are going to create tests in monkeyrunner otherwise our alternatives to verify some state in the views is limited to visual comparison. These cases were treated in previous articles like:




but now we will be using a logical approach rather than visual comparison. To be able to do it we need a mechanism of obtaining view properties values.
Lastest versions of monkeyrunner provides an extension to MonkeyDevice called EasyMonkeyDevice and this class has methods to get some properties like MonkeyDevice.getText().
This API is not documented so expect changes in the future.
TemperatureConverter, a sample application that has been used in other articles before, will be our application under test. The source code can be obtained from github.


The idea behind this test is, using monkeyrunner to connect to  the device, enter 123 in the Celsius field and expect to find 253.40 in the Fahrenheit field.

#! /usr/bin/env monkeyrunner

import sys
from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice
from com.android.monkeyrunner.easy import EasyMonkeyDevice
from com.android.monkeyrunner.easy import By

# connect to the device
device = MonkeyRunner.waitForConnection()

# start TemperatureConverter
device.startActivity(component="com.example.i2at.tc/.TemperatureConverterActivity")

# use the EasyMonkey API
easyDevice = EasyMonkeyDevice(device)

celsiusId = By.id('id/celsius')
if not celsiusId:
   raise Exception("View with id/celsius not found")

fahrenheitId = By.id('id/fahrenheit')
if not fahrenheitId:
   raise Exception("View with id/fahrenheit not found")

MonkeyRunner.sleep(3)
easyDevice.type(celsiusId, '123')
MonkeyRunner.sleep(3)

celsius = easyDevice.getText(celsiusId)
fahrenheit = easyDevice.getText(fahrenheitId)
expected = '253.40'

if fahrenheit == expected:
   print 'PASS'
else:
   print 'FAIL: expected %s, actual %s' % (expected, fahrenheit)

Unfortunately, it won't work in most of the cases. You are likely to receive this exception:


        java.lang.RuntimeException: java.lang.RuntimeException: No text property on node

I felt frustrated at first, but what the advantage of an Open Source project is other than going to the source code and find out why it's not working.
I dug into the Chimpchat, HierarchyView and ViewServer code and found out that for some reason EasyMonkeyDevice is looking for the text:mText property when in most of the cases it should be only mText.
So here is the patch, that I will be uploading to android soon:

diff --git a/chimpchat/src/com/android/chimpchat/hierarchyviewer/HierarchyViewer.java b/chimpchat/src/com/android/chimpchat/hierarchyviewer/HierarchyViewer.java
index 6ad98ad..6c34d71 100644
--- a/chimpchat/src/com/android/chimpchat/hierarchyviewer/HierarchyViewer.java
+++ b/chimpchat/src/com/android/chimpchat/hierarchyviewer/HierarchyViewer.java
@@ -170,7 +170,11 @@ public class HierarchyViewer {
         }
         ViewNode.Property textProperty = node.namedProperties.get("text:mText");
         if (textProperty == null) {
-            throw new RuntimeException("No text property on node");
+            // dtmilano: give it another chance, ICS ViewServer returns mText
+            textProperty = node.namedProperties.get("mText");
+            if ( textProperty == null ) {
+                throw new RuntimeException("No text property on node");
+            }
         }
         return textProperty.value;
     }
Once this patch is applied and you rebuild monkeyrunner or the entire SDK if you prefer, you will be presented with the expected result: PASS


UPDATE
This patch has been submitted to Android Open Source Project as https://android-review.googlesource.com/31850


UPDATE: July 2012
Android SDK Tools Rev 20 includes the aforementioned patch and now the previous monkeyrunner example works! 

Saturday, January 21, 2012

Automated Android testing using Sikuli

Sikuli is a tool that can supplement you testing toolbox.
It is a visual technology to automate and test graphical user interfaces (GUI) using images (screenshots). The scripts you create with this tool are in the Sikuli Script language, which is a Python (Jython) extension. It also features Sikuli IDE which is an integrated development environment for writing visual scripts with screenshots easily.

There are plenty of examples and tutorials in its web site but most of them are intended for desktop operating systems. As usual here we will be focusing on its Android praxis.


Unlocking the emulator
Out example will be unlocking the emulator screen using some screenshots. That is, instead of guessing or finding out a-priori the coordinates of the touch events that are needed to achieve the goal of unlocking the screen we will use images.
The Sikuli IDE provides the means of obtaining the screenshots to complete the arguments of the specific methods.
For example, if you select the click() method to click over something, you are prompted to take a screenshot of an area of the screen that will be the target and the the IDE shows this thumbnail.
Instead of trying to describe it, it's worth showing you the IDE window to understand what I mean.




The idea is to click on the Android text, just to gain windows focus in case it was lost. The specific method to do this is App.focus("5554") assuming that "5554" appears in the emulator's window title, work on Windows and Linux but it doesn't work on Mac OSX.
Then we search the screen for the image of the lock button, when we find it we touch with the mouse, wait a little, and drag till the position of the unlock dot, where we move up.






Unlocking a pattern lock
The previous example was very illustrative, but sincerely it was a bit simple and could have been done with other tools too.
So, let's get things a bit more complicated as it is the phone locked by a pattern.
As before we take the screenshot of the pattern but this time we double click on it in the IDE to get the Pattern Settings window and there we set the Target Offset to follow the shape of the pattern. This is then identified by a red cross over the screenshot as follows:




When you run this script you can see how the pattern is completed and consequently the phone unlocked.


Pretty simple, right ?
Definitely something you should consider when automating GUI tests.