In this short article, I will explain how to create a test automation framework for iOS using Sikuli, iOS simulator and Python unit test. I assume prior knowledge about these technologies and only focus on the actual technical details so that you hit the ground running.
- Sikuli API is distributed as a Java JAR file. In order to consume Java from Python you need to install Jython, the Java based Python interpreter. You can get it from here
- In order for Jython scripts to import Sikuli API you need to point to the Sikuli Jar file. Here is an example:
- Create a shell script to fetch the latest code of your iOS app from your preferred source control system, build it locally, deploy it to the iOS simulator then launch the app. I will include some of the commands that you may need.
- To make sure that you are using the right Xcode version apply the following command:
sudo xcode-select --switch /Applications/Xcode.app
- To build your app from the command line you may use something like:
xcodebuild -sdk "iphonesimulator8.2" -project my_project.xcodeproj build
- To launch the simulator from the command line do:
xcrun instruments -w $SIMULATOR -t Network
where $SIMULATOR can be retrieved by the following command:
xcrun instruments -s | grep Simulator
- To install the app that you just built use:
xcrun simctl install $SIMULATOR my_project.app
xcrun simctl uninstall $SIMULATOR com.your_company.your_app
or whatever ID the app may have.
- Here is a sample Jython script that uses Python unittest which invokes Sikuli for image based testing:
# Import python unit tests.
# This is needed to create test suites and test cases.
# Import sikuli Api
from org.sikuli.script import *
# This is where you store your screenshots
# Remember to take these screenshots using the Sikuli IDE
img_dir = '/Users/me/projects/visual/images/small/'
# This is where to store your test reports
reports_dir = '/Users/me/projects/visual/reports'
# Create a global Sikuli screen object
S = Screen()
# This method keeps waiting for an image to show up then clicks it.
# It times out after 40 trials. It can highlight the image found
# if needed. You can also set the similarity factor when searching
# for images.
def waitImage(image, highlight=False, time_out=40, similarity=0.99):
counter = 0
img_found = None
while img_found == None and counter < time_out:
print '..... Waiting for image [' + image + ']'
img_found = S.exists(Pattern(image).similar(similarity), 1)
if img_found != None:
print('***** Found image [' + image + ']')
print('????? Could not find image [' + image + ']')
# This method is called before your run
# your suite of test cases. For example you can launch
# the app in this module if you wish. In this case we are
# clicking the iOS app icon. Note that this makes
# sense only if you have more than one class of test cases otherwise
# if you have only one class then a setup class and teardown class is
# all what you need.
printLine('Module Setup', False)
# Click app icon
R = S.exists(Pattern(img_dir + "icon.png").similar(0.99), 1)
print('***** Found app icon, trying to click it...')
# Any cleanup on the module level is put here
# This is the base test case. You can specify the details of
# the shared setup and teardown here.
# This method executes before any test case runs
# This method executes after all test cases finish.
# In this case, I am assuming my iOS app has a play button
# and we play a video before each test case runs.
# Click play button
waitImage(img_dir + "play.png")
S.click(Pattern(img_dir + "play.png").similar(0.95))
# After each test case we close the video
# Click close
waitImage(img_dir + "close.png")
S.click(Pattern(img_dir + "close.png").similar(0.95))
# You can skip a class of test cases or a single case by
# un-commented the skip below. This is a class of test cases called Sanity.
# All test cases in this class have the setup and teardown defined earlier
# This is the first test case,
# it checks if the app plays a video normally.
printLine('001 - Video Plays Normally')
# Wait for a given frame in the video
self.assertNotEqual(waitImage(img_dir + "frame.png", True), None)
# This is where the script starts running,
# it uses a test running that generates
# xml output that is compatible with Jenkins CI
if __name__ == '__main__':
Please use the comments section below for questions, corrections or feedback. Thanks for reading.