Appium Notes 1698993757
Appium Notes 1698993757
Appium Notes 1698993757
WITH APPIUM
By Jonathan Lipps
Ruby Edition 2018.1
TA B L E O F C O N T E N T S
7 Ch. 2: Getting Set Up 36 Ch. 8: Running Tests in the Sauce Labs Cloud
This little e-book will help you get started with Appium using the Ruby
programming language. It is a complete revision from the ground up of an
earlier guide written by Dave Haeffner, who along with Matthew Edwards
deserves a lot of thanks for all the work put into the first edition.
Appium is an open source project that is always changing, and guides like
this one will never be accurate forever. When possible I will indicate which
versions of various software are being used, which might help in ensuring
reproducibility of the code samples used here.
As the Appium project lead, I benefit from the work of the entire community
in being able to write a guide like this. Appium would not be what it is today
without the maintainers and users who have decided to throw their lot in
with our take on mobile automation. The credit for this book as well as for
Appium as a whole go far and wide! Thanks especially to Sauce Labs who
commissioned the writing of this guide, and @KazuCocoa, the current
maintainer of the Appium Ruby libraries.
Jonathan Lipps
February 2018
Vancouver
3
Chapter 1
INTRODUCTION
Appium is a tool for automating apps. It has two components: the Appium
server, which does the actual automating, and a set of Appium clients, one
for every popular programming language. You write tests in your favorite
language by importing the Appium client for that language and using its API
to define a set of test steps. When you run the script, those steps are sent
one-by-one to a running Appium server, which interprets them, performs
the automation, and sends back a result if appropriate.
There are several kinds of mobile apps, and Appium lets you automate
all of them:
1. Native apps — apps built using the native mobile SDKs and APIs
3. Hybrid apps — apps with a native container and one or more webviews
embedded in that container. The webviews are little frameless browser
windows which can show content from the web or from locally-stored
HTML files. Hybrid apps allow the use of web technologies within a
native-like user experience.
What this means for you is that you are not just using Appium. You’re using
Appium in conjunction with one or more drivers. Even one platform (like
Android), might have multiple supported Appium drivers, which target
different fundamental automation technologies. For example, you can pick
between the appium-uiautomator2-driver and the appium-espresso-
driver when it comes to writing your Android tests. It’s worth getting to
know the different drivers so that you’re sure you’re using the best one for
your tests. While Appium does its best to ensure automation commands do
the same thing across different drivers, sometimes underlying differences
make this impossible. For the Appium code samples in this guide, the iOS
driver we’ll be using is appium-xcuitest-driver, and the Android driver
will be appium-uiautomator2-driver.
5
The Appium Ruby client is not a standalone library: it is actually a wrapper
around the standard Selenium Ruby client. So if you’re already familiar with
the Selenium client, you’ll find it easy to understand the Appium version.
6
Chapter 2
GETTING SET UP
ASSUMED KNOWLEDGE
This guide is meant to be a reasonably in-depth introduction to Appium.
However we do assume certain kinds of knowledge. For example, we expect
that you know your way around the command line terminal on your Mac, and
already know the Ruby programming language well enough to follow along
with simple examples. If either of these assumptions are not true for you, stop
here and do some digging on the Internet until you’ve found a good tutorial
on those topics before you continue following this guide.
• Install Xcode’s CLI tools (you’ll be prompted the first time you open
a fresh version of Xcode)
• Install Homebrew
• Using the Android Studio SDK Manager, download the latest Android SDK,
build tools, and emulator images
7
• In your shell login file (~/.bashrc, etc…):
APPIUM SETUP
There are two ways to install officially released versions of Appium: either
from the command line via NPM or by installing Appium Desktop.
And the most recent version of Appium will be installed. You can then run
Appium simply by typing appium from the command line.
8
Once you’ve got it on your system and opened up, you should be greeted
with a screen like this:
Now you can simply hit the “Start Server” button, and you’ll see an Appium log
window open up:
When we eventually begin running tests, this is where you’ll see the output of
what Appium is doing. Reading the Appium server logs can be a very useful
way to understand what is happening under the hood! But for now, we just
9
want to make sure that everything is working. You can go ahead and stop the
server if you want, or leave it running for later.
To be sure everything was installed, run gem list | grep appium. You should
see output like the following:
appium_console (2.8.1)
appium_lib (9.10.0)
appium_lib_core (1.3.2)
PROJECT SETUP
For the rest of this guide we’re going to be working on a Ruby project, starting
from scratch. To get set up for the project, create a new directory somewhere
10
on your system. It doesn’t matter where it is. But in this guide, we’re going to
pretend it is /path/to/project, so anytime you see that path, just replace it
with the one you’re using.
First, create two subdirectories, one called ios and one called android.
Then, download a copy of the test app we will use for this project. The test
app is called The App and it’s a silly little thing that will help us get going with
automation. There’s a version for both iOS and Android Download each app
from the v1.2.1 release page on GitHub. Put the iOS app (the one ending with
.app.zip) in the ios dir, and the Android app (the one ending with .apk) in
the android dir. At this point your project directory should look like:
/path/to/project
android
TheApp-v1.2.1.apk
ios
TheApp-v1.2.1.app.zip
We’re now ready to begin! Head on over to the next chapter where we
discuss how to open up your app and look for UI elements inside.
Library Version
Android 8.1, 7.1
Appium 1.7.2
Appium Ruby Client 9.10.0
iOS 11.1
Java JDK 1.8.0
Jenkins 2.89.4
macOS 10.13.3
Ruby 2.4.1
The App 1.2.1
Of course, I’ll try to write code that won’t go out of style too quickly.
But Appium is a fast-moving project, so some Appium code might become
outdated before too long, if you’re keeping up with Appium server and
driver releases. Always make sure you’re reading the most recently
published version of this guide.
11
Chapter 3
Before you can test your app, you have to know how it’s put together! You
don’t need to know the nitty-gritty of the app code, but you do need to
know what the UI elements are that your test will operate on. In this chapter
we’re going to look at two ways of exploring the element hierarchy of our
app, and figuring out how to find specific elements for use in testing. The
first way is via the command-line Appium console, and the second is via the
visual inspector bundled with Appium Desktop. (Be sure to read through both
sections even if you only care about one, since I explain more about how
Appium works along the way).
This means Appium is alive and waiting for a new automation session to be
requested. We’re going to do that with the appium_console gem, which has
installed an executable on your system called arc (appium ruby console). In
order to start a session using arc, we have to create a configuration file to
encapsulate the parameters we will use to start the session. In WebDriver-
land, these parameters are known as desired capabilities, often abbreviated
“caps”. We’re going to start by using 5 desired capabilities:
or “Android Emulator”)
• app: the path on your filesystem to the app you want to automate
12
• automationName: which Appium driver to use (if different than the default
In order to get arc to use these caps to start an Appium session, we create
a file for it to read, called appium.txt. We’re going to need two of these files:
one for our iOS app and one for our Android app. So fill out an appium.txt
file with the following contents, in the appropriate project directory.
In /path/to/project/ios/appium.txt:
[caps]
platformName = “iOS”
platformVersion = “11.2”
deviceName = “iPhone 7”
app = “/path/to/project/ios/TheApp-v1.2.1.app.zip”
[appium_lib]
sauce_username = false
In /path/to/project/android/appium.txt:
[caps]
platformName = “Android”
app = “/path/to/project/android/TheApp-v1.2.1.apk”
automationName = “UiAutomator2”
[appium_lib]
sauce_username = false
The only non-obvious differences between these two sets of caps are (1) in
Android, we don’t need to specify the platformVersion, since Appium will
just use the running AVD we have created, and (2) in Android, we make sure
to use the newer Android driver (namely appium-uiautomator2-driver).
server and not on Sauce. If you’re already a Sauce user reading this guide,
you might have certain environment variables set on your system that would
otherwise trigger the Ruby client to try to run the test on Sauce. Not so fast,
Ruby client!
13
Anyway, at this point, your project structure should look like:
/path/to/project
android
appium.txt
TheApp-v1.2.1.apk
ios
appium.txt
TheApp-v1.2.1.app.zip
cd /path/to/project/ios
arc
At this point, arc will attempt to start an iOS automation session on the
running Appium server, using the caps you provided in the appium.txt file.
If all goes you well, you will see a bunch of debug text filling the Appium
server log window, letting you know that Appium is working on starting
your session. If that process is successful, eventually an iOS simulator will
pop up and load our test app, and wait for further instruction from you. If
this does not happen, there will be error output from arc. Read it carefully,
as it will likely contain a clue as to what went wrong (maybe the path to
the application was not correct, for example). If it doesn’t, move over to
the Appium logs and read up from the bottom; likely there will be an error
message that could contain more information. (If you end up stuck, head to
the Appium forums and ask for help. You won’t be able to continue this guide
until you’ve got a working session!)
Once the session has started, arc will show you a prompt that allows
you to start typing:
[1] pry(main)>
At this prompt you can use API methods from the Ruby client in order to
find elements or examine the source tree of your application. What sorts
of elements are there? Let’s run the page_class command to find out:
14
pry(main)> page_class
17x XCUIElementTypeOther
2x XCUIElementTypeWindow
1x XCUIElementTypeStatusBar
1x XCUIElementTypeStaticText
1x XCUIElementTypeNavigationBar
1x XCUIElementTypeApplication
XCUIElementTypeOther
visible: true
XCUIElementTypeOther
visible: true
XCUIElementTypeOther
visible: true
XCUIElementTypeOther
isible: true
XCUIElementTypeOther
visible: true
XCUIElementTypeOther
visible: true
15
There are a number of locator strategies:
Strategy Description
:accessibility_id Accessibility label
:id Internal id
:class_name UI class name
:xpath XPath query based on source XML
Let’s try out the first strategy, :accessiblity_id, which is always the
recommended strategy since accessibility IDs can be added to elements
by the app developer across platforms, so they’re a good choice for a cross-
platform locator. From the output of the page command earlier, we know
that there is an element with the name Echo Box. On iOS, the accessibility
ID is shown as the name attribute, so that’s what we use:
#<Selenium::WebDriver...>
“Echo Box”
Good. The fact that the .text command came back with “Echo Box” meant
we found the element we were looking for. If we wanted, we could also
perform an action on this list item, maybe tapping on it:
“”
If you watch the simulator while running this command, you’ll see the view
change as a result of the list item being tapped. We get an empty response
from arc because the click action doesn’t return any value; it simply does
its thing and gives back control. Let’s finish out this section by closing the
session (don’t want to leave it hanging around blocking future sessions or
hogging resources). We can simply type x at the arc prompt to achieve this.
pry(main)> x
16
Believe it or not, this is basically everything you need to know about how to
write an Appium test. Every Appium test consists of the following components:
2. Finding elements
Steps 2 and 3 simply repeat as many times as necessary to walk through your
app and generate the desired user behavior, making verifications in your test
code along the way. Of course, there’s a lot more to learn in terms of library
methods available to you. But this is the basic pattern. The rest is details!
Now, it’s up to you to go explore the Android version of The App. All you need
to do is fire up your AVD from Android Studio, and then run arc from the
android subdirectory in your project. If all goes well, you’ll be able to explore
the Android version of this app in exactly the same way as we did for iOS.
commands to see how they work, or try something out without having to
write a whole test. But for inspecting your app, finding elements, and a host
of other reasons, Appium Desktop is the tool of choice.
If you’ve already got Appium Desktop’s server running, simply tap the
magnifying glass icon (“Start Inspector Session”), and a new window will
pop up that will give you the ability to enter desired capabilities for a new
Appium session:
17
Using this UI, port over the desired capabilities from the iOS appium.txt. As
you build the caps, you’ll see a nice JSON representation so you can double-
check your work. If you want, you can even save this set of caps so you can
load it next time you launch Appium Desktop. When you’re done, ensure that
the server selection panel has the correct server set (probably the “Automatic
Server” if you’re running from Appium Desktop). Then, click “Start Session”.
At this point Appium Desktop is starting a new session for you using the
provided capabilities. If all goes well, the window will morph into the Inspector:
In the screenshot section, you can move your mouse over different
elements and see them highlighted. If you click on one, it will open up in
the property viewer. Go ahead and click on the “Echo Box” list item. You’ll
see it focused in the source tree, and you’ll also see a bunch of information
retrieved for the element in the property viewer. In the upper portion of
the property viewer, there’s even a small table with recommended locator
strategies and selectors for this element. In this case, Appium Desktop is
recommending that we find this element by ‘accessibility id’, using the same
selector we used within arc. Just like in ‘arc’, we can also run commands
18
on this element. By hitting ‘Tap’, we see the same action as before: the list
item is tapped and we get to a new view. After this new view loads, the
screenshot and source refresh so we can explore the current state. (If the
app is slow and the refresh happens before new elements load, you can
always hit the ‘Refresh’ button to update the Inspector).
Appium Desktop’s Inspector is a powerful tool with many more features than
we can go into in this guide, including the ability to record test actions and
generate usable code. Please play with it and check out the Appium Desktop
README for more information. What we’ve learned to do with it so far is still
absolutely essential: how to navigate our app’s hierarchy and figure out which
selectors we should be using in our test code.
Speaking of test code, it’s high time we wrote some. Now that we’re armed
with the ability to figure out which elements are in our app and how to find
them, we can move to our favorite editor and write some Ruby scripts that
actually perform a useful verification.
19
Chapter 4
The test we’re going to write in this chapter will exercise the admittedly fake
“login” functionality of The App. It’s worth thinking first about how we would
test this manually before we begin to write code. Here’s what we’d do to
verify a successful login flow:
5. Verify that we can see something only logged-in users should see.
(Of course, to fully exercise the feature we’d also want a negative test:
entering an invalid username or password and ensuring that we cannot
see the logged-in area.)
Let’s dive into the Ruby code that will test this flow. First of all, because
we want to assume we may be working on this test code as a team at
some point in the future, we want to ensure that all the versions of our
dependencies are set. We wouldn’t want a teammate to try to run the test
with a different set of gems, since it could lead to hard-to-debug errors.
The way to achieve consistency here is to use the bundler gem. Install it
if you don’t have it already:
Then, create a file called Gemfile in the root of our project directory.
The Gemfile will hold the description of the gems and their versions:
source ‘https://rubygems.org’
With this file in place we can run the bundle command in the project
root directory:
bundle install
20
This will ensure we have all the gems installed at the appropriate versions,
including the new rspec gem. RSpec is the test framework we will be using
to develop our test suite. Now let’s compose our first test file, for iOS.
Create a file called login_spec.rb and add it to the ios directory:
require ‘appium_lib’
describe ‘Login’ do
before(:each) do
@driver = Appium::Driver.new(caps)
@driver.start_driver
end
after(:each) do
@driver.driver_quit
end
@driver.wait {
}.click
username = @driver.wait {
@driver.find_element(:xpath,
“//XCUIElementTypeTextField[@name=\”username\”]”)
password = @driver.find_element(:xpath,
“//XCUIElementTypeSecureTextField[@name=\”password\”]”)
username.send_keys(“alice”)
password.send_keys(“mypassword”)
@driver.find_element(:accessibility_id, “loginBtn”).click
@driver.wait {
@driver.find_element(:accessibility_id,
end
end
21
At this point, your directory tree should look like:
/path/to/project
android
appium.txt
TheApp-v1.2.1.apk
Gemfile
Gemfile.lock
ios
appium.txt
login_spec.rb
TheApp-v1.2.1.app.zip
You can run the test by navigating to the ios directory in your terminal
(if you’re not still there), and running:
rspec login_spec.rb
Assuming all has been set up correctly, you’ll see Appium navigating the
views, filling in the username and password details, and ending the session.
Now let’s pick apart the code to see how it all worked! First, we make sure
to require our Appium library, and set up the skeleton for our test using the
RSpec structure:
require ‘appium_lib’
describe ‘Login’ do
before(:each) do
...
end
after(:each) do
...
end
it ... do
...
end
end
What this structure indicates is that we have one or more tests belonging to
a ‘Login’ category. We want RSpec to run some code before and after every
single test, which we put into the before(:each) and after(:each) blocks.
Finally, we specify the test itself using the it block. The way we’re using this
structure is to set up our Appium session in the before block, use the session
for the test, and then tear it down in the after block. When we have multiple
tests, we are going to be doing this for each test, so that every test has its own
completely fresh Appium session.
22
How do we start an Appium session? Let’s take a look:
@driver = Appium::Driver.new(caps)
@driver.start_driver
We’re taking advantage of the fact that we already have our desired
capabilities set in the appium.txt file, and simply loading the caps from the
file using the Appium.load_appium_txt helper utility. Then, we’re storing a
new Appium::Driver based on these caps into a Ruby instance variable, and
starting the session with it. We’re making an instance variable here because
we want the driver to be accessible from other parts of our script (the test
block itself, most importantly).
The way we end an Appium session is dead simple: just call driver_quit
on our stored @driver object.
@driver.wait {
}.click
username = @driver.wait {
@driver.find_element(:xpath,
“//XCUIElementTypeTextField[@name=\”username\”]”)
password = @driver.find_element(:xpath,
“//XCUIElementTypeSecureTextField[@name=\”password\”]”)
username.send_keys(“alice”)
password.send_keys(“mypassword”)
@driver.find_element(:accessibility_id, “loginBtn”).click
@driver.wait {
@driver.find_element(:accessibility_id,
I’m making heavy use of a driver method called wait, which takes a block
and returns (in this case) an element. What is the purpose of wait? When
you ask Appium to find an element, it will do so immediately, exactly when
you ask it to. That’s all well and good, except for the fact that sometimes
your app is not quite in a state where the element you are trying to find
is ready. This can happen during view transitions, or while waiting on a
network request, or for many other reasons. As humans, we’re pretty good
at watching the view until we notice it’s in a good state for interaction.
23
As a robot, Appium is less sophisticated, and we have to be explicit about
telling Appium to wait until conditions are right. We wouldn’t want this test
to fail just because Appium was too eager to try and find an element, and
then declared it to be non-existent, when it just needed to wait a few more
milliseconds! This is exactly what wait does: continually retries whatever is
in the block for up to 30 seconds by default.
What the rest of the code does should be pretty self-explanatory given what
we learned in the previous chapter. We’re finding elements using a variety
of locator strategies, and performing actions on them. The new action we
encounter in this code is send_keys, which sends keystrokes to an input field.
The only thing apparently missing from this test is a verification. How are we
proving that we have navigated to a logged-in area? Actually, a verification
is hidden in the last chunk of the method. Telling Appium to wait until
an element exists with the string “You are logged in as alice” is a
verification, because if the element is present, we know we have reached the
logged-in area. If the element is not present, the wait will eventually throw
an error, and the test will be considered to have failed as a result.
This is a great start, but we can certainly do better with the code. We’ll see
how in the next chapter.
24
Chapter 5
I N T R O D U C T I O N T O PA G E O B J E C T S
One issue with the test code we’ve written as it stands is that we are mixing
information about our app (namely which elements can be found with which
locators) and information about our test (which test steps constitute the flow
we are trying to test). Another issue is that, as soon as we add a second login-
related test, we’ll begin duplicating our selector strings. Imagine if the app
were to change its accessibility IDs—we’d have to go make a change in many
different tests!
The common solution to these problems is to use something called the Page
Object Model. A Page Object represents a view, and exposes only high-level
actions so that test code can deal in user-level behaviors rather mixing in
low-level element finding logic. What do I mean by this? Imagine if the test
block from login_spec.rb were instead to read something like this:
@home_view.nav_to_login
@login_view.login(“alice”, “mypassword”)
name = @user_view.get_logged_in_user
assert_equal(user, “alice”)
This is a much more concise, readable, and maintainable bit of test code. It
specifies everything we need to do, and nothing more than that. It puts the
responsibility of finding elements or getting data from them on these new
objects we’re referencing: @home_view, @login_view, and @user_view. These
represent the Page Objects that I have been talking about. Let’s take a look at
what a Page Object might look like for the home view:
class HomeView
@@login = [
:accessibility_id,
“Login Screen”
def initialize(driver)
@d = driver
end
def nav_to_login()
end
end
class LoginView
@@username = [
:accessibility_id,
“username”
@@password = [
:accessibility_id,
“password”
@@login_btn = [
:accessibility_id,
“loginBtn”
def initialize(driver)
@d = driver
end
@d.find_element(*@@password).send_keys(password)
@d.find_element(*@@login_btn).click
end
end
This is a little meatier, but still nice and simple. We have information about
finding our elements, and then the high-level login method exposed. Let’s
round out our object models with the model for the logged-in user page:
class UserView
@@message = [
:xpath,
def initialize(driver)
@d = driver
end
def get_logged_in_user()
end
end
26
Since what we want is to provide a method which returns the name of the
logged-in user, we have to do some clever substitution on the text which
is actually available to us, but otherwise this Object Model is also very
straightforward. Assuming we have named these different files home_view.
rb, login_view.rb, and user_view.rb respectively, we can update our main
require ‘appium_lib’
require ‘test/unit/assertions’
require_relative ‘./home_view.rb’
require_relative ‘./login_view.rb’
require_relative ‘./user_view.rb’
include Test::Unit::Assertions
describe ‘Login’ do
before(:each) do
@driver = Appium::Driver.new(caps)
@home_view = HomeView.new(@driver)
@login_view = LoginView.new(@driver)
@user_view = UserView.new(@driver)
@driver.start_driver
end
after(:each) do
@driver.driver_quit
end
@home_view.nav_to_login
@login_view.login(“alice”, “mypassword”)
user = @user_view.get_logged_in_user
assert_equal(user, “alice”)
end
end
Despite the fact that we have some extra requires, our test code itself is now
much more maintainable and easy to understand. Furthermore, in future tests
we now have access to a growing library of high-level methods that we can
reuse. And if a selector changes for an app element, we have one place to go
to fix it without having to worry about where else it might be in our codebase.
Migrating to the Page Object Model was great, but we can do a bit more
cleanup. Right now our directory structure is getting a bit cluttered. Let’s
move the test file into its own directory called spec (paving the way for more
tests), and the Page Objects into their own directory called views. Once we do
that, we can also create a handy little file called requires.rb whose job is
27
simply to require everything we need so that we don’t have to have a bunch
of require calls cluttering up our test code. Assuming we’ve put requires.rb
alongside our test code inside spec, it should look like:
require ‘appium_lib’
require ‘test/unit/assertions’
require_relative ‘../../common.rb’
require_relative ‘../views/home_view.rb’
require_relative ‘../views/login_view.rb’
require_relative ‘../views/user_view.rb’
include Test::Unit::Assertions
Finally, our login spec is getting pretty lean. There’s still some boilerplate in
it, however, around setting up the driver. Even though it’s only a few lines,
once we have more tests, we’ll want to factor that out. Let’s do it now so
that when we take a look at writing an Android test, everything will be ready.
We can create a file called common.rb in the project directory, with the
following contents:
def setup_driver
Appium::Driver.new(caps)
end
Once we add this to our requires.rb, it’s ready to use in our login spec file,
which at last looks like this:
require_relative ‘./requires.rb’
describe ‘Login’ do
before(:each) do
@driver = setup_driver
@home_view = HomeView.new(@driver)
@login_view = LoginView.new(@driver)
@user_view = UserView.new(@driver)
@driver.start_driver
end
after(:each) do
@driver.driver_quit
end
@home_view.nav_to_login
@login_view.login(“alice”, “mypassword”)
user = @user_view.get_logged_in_user
assert_equal(user, “alice”)
end
end
28
If everything is linked together correctly, you should be able to run rspec
login_spec.rb in the ios/spec directory, and get the same passing testcase,
only now with a much more beautified architecture. For reference, your
project files should now look like:
/path/to/project
android
appium.txt
TheApp-v1.2.1.apk
Gemfile
Gemfile.lock
ios
appium.txt
common.rb
spec
login_spec.rb
requires.rb
TheApp-v1.2.1.app.zip
views
home_view.rb
login_view.rb
user_view.rb
29
Chapter 6
A N D R O I D J O I N S T H E PA R T Y
/path/to/project
android
common.rb
spec
requires.rb
<specs here>
views
<views here>
ios
common.rb
spec
requires.rb
<specs here>
views
<views here>
In other words, each of android and ios had their own whole test tree.
Since we want to share as much code as possible, we’re going to bring the
spec, views, and common code up top, and distinguish between iOS and
/path/to/project
apps
common
spec
android
base
ios
views
30
Essentially, we’ll be putting the test logic itself into spec files inside the base
dir, and merely referencing those from the android and ios spec files, which
will of course be neighbors to their platform-specific appium.txt and test
app. To make sure we share as much code as possible, we’ll also start using
RSpec’s configuration feature, which will hide away all the before(:each)
and after(:each) boilerplate. Let’s create a file inside our new common
directory called spec_helper.rb. (If you haven’t created the new directory
structure above yet, do so now). spec_helper.rb should look like:
require ‘rspec’
require ‘appium_lib’
RSpec.configure do |config|
config.before(:all) do
@driver = Appium::Driver.new(caps)
@home_view = HomeView.new(@driver)
@login_view = LoginView.new(@driver)
if caps[:caps][:platformName].downcase == “ios”
@user_view = IOSUserView.new(@driver)
else
@user_view = AndroidUserView.new(@driver)
end
end
config.before(:each) do
@driver.start_driver
end
config.after(:each) do
@driver.driver_quit
end
end
What we’re doing in this file is basically creating before and after blocks
that will be run in any spec file which includes this helper. There are two
areas of our test setup that are platformspecific: (1) our appium.txt file which
contains our platform-specific capabilities, and (2) the Page Object Models.
We differentiate which appium.txt we’re using automatically in virtue of
relying on Dir.pwd; whichever directory we’re running the rspec command
from is where we’ll look for appium.txt. So when running the command
from android, it’ll find the appropriate file to use for caps, and likewise for iOS.
As for the Page Object Models, we’ll have a closer look momentarily. Two
of the three models are completely cross-platform, whereas one has minor
31
differences between platforms, so we need to choose the right model based
on the capabilities we’re using for the test.
Great. The next bit of code reuse is to make sure that both iOS and Android
spec files use the same test code. Before, we had it in ios/login_spec.rb.
Now, we’re going to move it to spec/base/login.rb, and simply expose the
test as a plain old method:
require_relative ‘./requires.rb’
def login_with_valid_credentials
@home_view.nav_to_login
@login_view.login(“alice”, “mypassword”)
user = @user_view.get_logged_in_user
assert_equal(user, “alice”)
end
end
Notice that the method returns an it block. This is so we can simply call the
method from inside our spec file’s describe block without having to specify
the block again. Notice also that our requires.rb is now in this same base
directory. It’s basically the same, but with updated paths:
require ‘appium_lib’
require ‘test/unit/assertions’
require_relative ‘../../common/spec_helper.rb’
require_relative ‘../../views/home_view.rb’
require_relative ‘../../views/login_view.rb’
require_relative ‘../../views/user_view.rb’
include Test::Unit::Assertions
# /path/to/project/spec/ios/login_spec.rb
require_relative ‘../base/login.rb’
login_with_valid_credentials
end
# /path/to/project/spec/ios/login_spec.rb
require_relative ‘../base/login.rb’
login_with_valid_credentials
end
32
These two files are extremely short—a mere 4 lines of code, including the
require. We’re relying on our spec_helper to decorate the it block with
the appropriate before and after methods, and then we’re simply mixing in
the desired test behavior as the login_with_valid_credentials method.
As we write more tests, we can simply add their method calls here.
The last real difference we need to look at is the user_view.rb Page Object
Model. We now need two versions, because we can’t, unfortunately, use com-
pletely identical locators across platforms. We can, however take advantage of
Ruby’s class inheritance to only change what we absolutely must:
class IOSUserView
def message
[:xpath,
end
def initialize(driver)
@d = driver
end
def get_logged_in_user()
end
end
def message
[:xpath,
end
end
Here we’ve decided that the ‘default’ view is IOSUserView, and then we
extend it to AndroidUserView below. We’ve also changed from using class
variables (@@message) to instance methods, because class variables don’t play
nice with inheritance in Ruby. So what we end up with is a set of Page Object
Models that is platform-specific, but shares all the possible logic. We wire this
conditionality into our test via the spec_helper.rb code we saw above.
With this addition, our new full-blown project setup looks like:
33
/path/to/project
apps
TheApp-v1.2.1.apk
TheApp-v1.2.1.app.zip
common
spec_helper.rb
spec
android
appium.txt
login_spec.rb
base
login.rb
requires.rb
ios
appium.txt
login_spec.rb
views
home_view.rb
login_view.rb
user_view.rb
Gemfile
Gemfile.lock
Given that we’ve decided to put our apps in an apps directory, we’ll also
need to make the changes in appium.txt to point to /path/to/project/
apps instead of the platform-specific directory. Once you’ve done that, we
can test our refactoring to make sure we haven’t broken the iOS test:
cd /path/to/project/spec/ios
rspec login_spec.rb
And then, because we’ve made everything totally cross-platform, we can run
the same test on Android:
cd /path/to/project/spec/android
rspec login_spec.rb
Don’t forget to launch your emulator before attempting to run the test! And
that’s it. Of course, if you aren’t dealing with a cross-platform app in your own
testing, you may not want to go to such lengths to share code the way we did
here. We went through this exercise to drive home the point that it’s possible,
and a good idea, to think about your test code as a software product just as
much in need of refactoring and code reuse as your app itself. Keep it clean
and keep it pretty!
34
Chapter 7
So far we’ve been running our tests locally using the rspec test runner
directly. But Ruby projects often use a task runner called rake (Ruby’s version
of Make), which comes with some niceties for command line execution of
tasks like our tests. Let’s upgrade our project for use with rake.
source ‘https://rubygems.org’
Make sure to run bundle install at this point to get the new gem on your
system. Now we can build a Rakefile which specifies the kinds of tasks we
want rake to run for us:
task :ios do
Dir.chdir ‘spec/ios’
end
task :android do
Dir.chdir ‘spec/android’
end
This Rakefile tells rake that when we want to run the ios task, to first switch
to the spec/ios directory, and then to execute the rspec command with the
appropriate arguments to make it run any spec files it finds nearby. To make
sure our Rakefile is put together correctly, we can query it for tasks:
> rake -T
Go ahead and give each of these commands a try, to verify the tests still pass
successfully. Next, we’ll move on to adding more rake tasks and running our
tests in the cloud!
35
Chapter 8
There comes a time in the life of every testsuite when it becomes so large
that it’s impractical to run locally on your own machine, or requires platforms
you no longer have easy access to. For these and a variety of other reasons,
it’s a good idea to invest in cloud Appium providers like Sauce Labs. appium_
lib comes with built-in support for running tests in the Sauce cloud. To take
1. A Sauce Labs username and access key (if you don’t already have
one, you can start a free trial here, and find your access key at your
dashboard after login)
source ‘https://rubygems.org’
1. Host our app on the web somewhere and provide a url as the app capability
2. Use sauce_whisk to upload our app to Sauce with the Sauce Storage API
Let’s pursue option 2 here, since it is what you’d want to do in the context of
a CI server. What we’re going to do is upgrade our spec_helper.rb to include
Sauce-specific test setup and app uploading:
36
require ‘rspec’
require ‘appium_lib’
require ‘sauce_whisk’
def using_sauce?
user = ENV[‘SAUCE_USERNAME’]
key = ENV[‘SAUCE_ACCESS_KEY’]
end
def upload_app(app)
storage.upload(app)
“sauce-storage:#{File.basename(app)}”
end
RSpec.configure do |config|
config.before(:all) do
if using_sauce?
caps[:caps][:app] = upload_app(caps[:caps][:app])
end
@driver = Appium::Driver.new(caps)
@home_view = HomeView.new(@driver)
@login_view = LoginView.new(@driver)
if caps[:caps][:platformName].downcase == “ios”
@user_view = IOSUserView.new(@driver)
else
@user_view = AndroidUserView.new(@driver)
end
end
config.before(:each) do
@driver.start_driver
end
config.after(:each) do
@driver.driver_quit
end
end
What we’ve done is add two new methods, one to check if we want to run on
Sauce by querying the environment variables, and another to upload an app
to Sauce Storage using Sauce Whisk. We then use these methods to reset the
app capability to the Sauce Storage location if we’re running on Sauce.
Next, we need to update our appium.txt for both iOS and Android, ensuring
we’re using capabilities that are valid on Sauce. We also need to add the
appiumVersion capability, because when we run tests on Sauce, we can
37
How do we know what capabilities we should use to select the appropriate
Sauce platform? By using Sauce’s Platform Configurator, a handy tool to walk
you through getting the capabilities for the platforms you need. Using this
tool, I’ve updated the appium.txt files (and also removed our hack to prevent
tests from running on Sauce before we wanted to).
For iOS:
[caps]
appiumVersion = “1.7.2”
platformName = “iOS”
platformVersion = “11.1”
deviceName = “iPhone 7”
app = “/path/to/project/apps/TheApp-v1.2.1.app.zip”
[caps]
appiumVersion = “1.7.2”
platformName = “Android”
platformVersion = “7.1”
app = “/path/to/project/apps/TheApp-v1.2.1.apk”
automationName = “UiAutomator2”
38
desc ‘Run iOS tests’
setup_env args[:where]
Dir.chdir ‘spec/ios’
end
setup_env args[:where]
Dir.chdir ‘spec/android’
end
def setup_env(where)
if where != “sauce”
ENV[‘SAUCE_USERNAME’] = nil
ENV[‘SAUCE_ACCESS_KEY’] = nil
end
end
export SAUCE_USERNAME=”my_username”
export SAUCE_ACCESS_KEY=”my_access_key”
This will store the variables for the duration of the current terminal session.
If you want to store them permanently, it’s a good idea to add the export
commands to your ~/.bashrc, ~/.bash_profile, ~/.zshrc, etc…, shell login
file. That way you never have to remember them again.
Now, we can use our new rake task parameters to decide whether we want
to launch a Sauce session or a local one:
Go ahead and run the Sauce versions. While you’re running them, log onto
the Sauce Labs website and you can see the tests running on your dashboard.
If you click on a test, you will be greeted with a variety of details about
it, including a stream of the running test (or a video if you catch it after it
39
finishes). You might notice that the name of the test (unnamed test ....)
isn’t very descriptive, and that we see a nasty gray question mark even though
we know our test passed. Let’s fix that.
Let’s once again upgrade our spec_helper.rb. This time we just need to
modify the before(:each) and after(:each) blocks to take a parameter
we’ll call test. This is RSpec’s way of giving us access to information about
the testcase. We can use this information to set the name capability before
the test begins, and to set the status of the Sauce job after it has finished:
config.before(:each) do |test|
@driver.start_driver
end
config.after(:each) do |test|
if using_sauce?
SauceWhisk::Jobs.change_status(@driver.driver.session_id,
test.exception.nil?)
end
@driver.driver_quit
end
At this point, we’ve got a very robust and flexible setup that can work well for
local development as well as extend to the cloud. The last step in our journey
is to set our tests up to be run as part of a Continuous Integration server.
40
Chapter 9
A U T O M AT I N G T E S T R U N S W I T H A C I S E R V E R
The point of automating your tests is to have total flexibility in how and when
they are run. The ideal scenario is for your entire test suite to be run on every
single code change, so that changes are gated on passing the entire test suite.
Why allow code in to your app if it breaks something?
The way many teams work this out in practice is by using a “CI server”.
CI stands for “Continuous Integration”, and refers to the process whereby
new code is constantly added to the shippable version of your app. Typically
developers use a branching version control system with one branch (master
or trunk) always representing a known-working increment of the app.
One popular open source CI server is Jenkins, and we’ll set up a local version
of Jenkins in this chapter, to see how easy it is to configure tests to run in CI.
SETTING UP JENKINS
Jenkins runs on any platform, and at some point you may want to run it in a
Linux container or on a Linux host, but for now we will stick with macOS so
that we can run Jenkins locally. There are a number of ways to install Jenkins,
but we’re going to stick with homebrew to make things easy:
jenkins
Jenkins will go through its startup routine and automatically launch itself
on port 8080, so you can open up a browser and navigate to
41
http://localhost:8080. Before you do that, take a look at the CLI output
from the Jenkins startup, and copy the admin password you will need to log
in. Now head to your web browser and launch the URL.
At this point Jenkins will guide you through a little setup flow. First, paste
in the admin password you copied from the command line, or cat it out
using the terminal in order to set up the admin account. Then, bypass the
“Customize Jenkins” wizard by clicking the close button at the top right.
You might find it valuable to explore the ecosystem of Jenkins plugins after
you’re done with this guide.
2. For the item name, enter “Android Appium” (or some other clever moniker
not suitable for publishing in a guide like this)
4. Click “OK”
The result will be a page with a host of options. This is where we would teach
the Project how to read from our version control system, or when it should
run itself (either via a remote trigger or on a schedule), etc… For now, let’s just
focus on the Build step itself:
This will open up a text input area where we can type commands as if we
were running them from the terminal. Enter the following commands:
cd /path/to/project
bundle update
rake android
These are the same commands we would run if we were a new user getting
started with running tests. Now let’s run our Project. Head back to the main
dashboard, and navigate to the Project we just created. On the left sidebar,
click “Build Now”. You’ll see a little progress indicator pop up with a build
number by it (#1). This is a “Job” representing an instance of building the
42
Android Project you created. At the Job page, you’ll see its status, and you
can follow along with what’s happening by clicking on “Console Output”
on the left sidebar. If all goes well, you’ll be greeted by this output:
+ cd /path/to/project
+ bundle update
Resolving dependencies...
Bundle updated!
+ rake android
1 example, 0 failures
Finished: SUCCESS
43
This is the log of what happened during the job. As you can see, we made
sure dependencies were up to date (in case our Gemfile had changed since
the last build), and ran the android tests locally.
3. This time, start typing the name of the Android project in the “Copy from”
field, and select the existing project
5. Modify the shell commands, substituting rake ios for rake android
6. Save the Project, and click “Build Now” from the Project page
Copying from an existing Project the way we did means we could rely on the
existing shell commands and not have to start from a blank slate.
RUNNING ON SAUCE
Creating a project to run on Sauce is just as easy as running locally.
To see how:
1. Follow the same steps as in the previous section to copy one of the
existing Projects to a new Project called “Appium on Sauce”
2. In the shell command box, replace the existing rake command with two
new ones: rake ios[‘sauce’] && rake android[‘sauce’]
Now build this new Project and watch your tests run on Sauce!
For a real CI setup, you might consider checking out the Sauce Jenkins plugin
which comes with a number of helpful features to make the experience of
running on Sauce more integrated with your Jenkins server.
44
JENKINS FOR PRODUCTION
It’s easy to see the power of something like Jenkins, once you imagine having
these tests kicked off whenever a new commit comes in, or on an automated
schedule of some sort. You have only to log back into your Jenkins server and
see how your builds are doing, or explore the logs to debug any failures.
What we’ve done in this guide is run Jenkins as a toy under our own user. This
would not be advisable in production. Instead, you’d want to follow one of
many guides online (like this one) about setting Jenkins up for the appropriate
host type in a secure and reliable fashion.
We also took advantage of the fact that Jenkins was running under our
system user in order to keep our shell commands simple. In a real production
setup, we’d have to worry about setting environment variables correctly
(remember how we set $ANDROID_HOME, and all the rest? That needs to be
done for Jenkins too). We’d also have to worry about emulator management
for Android (for example, creating and tearing down emulators to ensure
complete data isolation between builds), dependency management (ensuring
Android and Xcode stay updated on build machines), etc… It’s a lot! But
getting CI set up for your team is always worth the effort.
45
Chapter 10
3. How to interrogate iOS and Android apps using the command line and
the Appium Desktop Inspector
4. How to write simple tests in Ruby and RSpec using the Appium Ruby client
5. How to refactor and organize test code, leveraging Page Objects to make
it fully crossplatform
7. How to run tests in the cloud on Sauce Labs, leveraging different aspects
of the Sauce API
I hope you found it valuable, and I’ll leave you with a few resources for further
engagement with Appium.
RESOURCES
• The Appium Discussion Group is a great place to go and ask other
Appium users for help
• Appium’s GitHub Page contains links to the source code for the (many)
Appium packages that make up the Appium server and clients
• Appium’s Issue Tracker is where to go if you think you’ve found a bug and
want to report it
46
SUPPORT
Appium is community-supported via the discussion forums and GitHub.
Sauce Labs customers can receive support for Sauce-related Appium issues
via their account manager. And of course, there are a number of experienced
Appium consultants out there who would be happy to offer paid support for
tough issues.
EB-03-042018
47
ABOUT SAUCE LABS
Sauce Labs ensures the world’s leading apps and websites work flawlessly on every browser, OS and device. Its award-
winning Continuous Testing Cloud provides development and quality teams with instant access to the test coverage,
scalability, and analytics they need to deliver a flawless digital experience. Sauce Labs is a privately held company
funded by Toba Capital, Salesforce Ventures, Centerview Capital Technology, IVP and Adams Street Partners. For more
information, please visit saucelabs.com.
SAUCE LABS INC. - HQ SAUCE LABS EUROPE GMBH SAUCE LABS INC. - CANADA
116 NEW MONTGOMERY STREET, 3RD FL NEUENDORFSTR. 18B 134 ABBOTT ST #501
SAN FRANCISCO, CA 94105 USA 16761 HENNIGSDORF GERMANY VANCOUVER, BC V6B 2K4 CANADA