Automate an iOS Tic Tac Toe app with Appium 2 (XCUITest) Part 1: Player vs Player
How to use Appium 2 to automate playing Tic Tac Toe
In this article, I’ll show you how to automate Nathan Fallet and Group MINASTE’s Tic Tac Toe iOS app with Appium 2.
Table of Contents:
· Setup
∘ Install Appium 2 and the XCUITest Driver
∘ Build the Tic Tac Toe app
· Get Started
∘ How to Play Tic Tac Toe
∘ How to Play Tic Tac Toe with Appium
∘ Refactoring
· Complete Test Scripts
Setup
Install Appium 2 and the XCUITest Driver
Follow the steps in my other article to install the Appium 2 server, the XCUITest Driver and your desired Appium Client library (I prefer Ruby).
A basic summary of the setup (for macOS) is as follows:
# Install Node.js
brew install node@18
# Install Appium 2
npm install -g appium@next
# Install XCUITest Driver
appium driver install xcuitest
# Run this in a separate window to start up the Appium server
appium
# Install Ruby's Appium client library
gem install --no-document appium_lib
Note that there are some security permissions that may need to be granted. See the article for detailed step-by-step instructions as well as a simple script to verify the setup.
Build the Tic Tac Toe app
In this exercise, I will run a Tic Tac Toe app on the iOS simulator, using the open-source tic tac toe app I found on Github by Group MINASTE. By the way, this app is also available on the App Store under the name Tic TAI Toe
.
Clone or download the project (from Github) then build it in Xcode. Once the project has been built, we want to get it in .app.zip
format.
Navigate to ~/Library/Developer/Xcode/DerivedData/
to find your app name, just check the starting part. For example, “MorpionTPE-dhzxccjhkowtjtfegrbiqgmnxewt
”. Under that folder, go to Build/Products/{another folder named after your build target}
to find the app file. Right-click and “Compress” to zip it, then rename it to MorpionTPE.app.zip
.
Now that we have the app file and Appium set up, we can start playing the game with Appium!
Get Started
Create your test project, it can be done in your favourite editor and run on the command line. I used TestWise, a testing IDE as it’s very helpful to set up the initial Appium options.
In TestWise, you can select an Appium project and specify the app path:
TestWise will create a project skeleton with the Appium capability options to start up the TicTacToe app.
# test_helper.rb
def app_file_path
'/Users/courtney/sample-apps/MorpionTPE.app.zip'
end
def appium_xcuitest_opts
opts = {
caps: {
platformName: 'ios',
automationName: 'xcuitest',
platformVersion: '16.2', # change to match your Xcode
deviceName: 'iPhone 14', # change here if necessary
app: app_file_path
},
appium_lib: {
server_url: "http://127.0.0.1:4723",
wait: 0.1,
},
}
end
def appium_opts
return appium_xcuitest_opts()
end
Then, at the start of every file, it starts the driver with the above settings:
before(:all) do
@driver = Appium::Driver.new(appium_opts).start_driver
end
Now that our test’s skeleton is ready, we can start driving the Tic Tac Toe app.
Right-click the sample test case (new_spec.rb
) and select Run "..."
, you will see the app launching in an iOS simulator.
How to Play Tic Tac Toe
Given a 3x3 board, two players take turns selecting a cell. First to 3-in-a-row wins. The app let’s you pick between three game modes:
In this exercise, I’ll select Player vs Player (PVP) mode, i.e. we can control the outcome (via scripts). Let’s play a very simple game so that Player 1 (X) wins.
The game plan:
# Begin game
Player 1: (1, 1)
Player 2: (3, 1)
Player 1: (1, 2)
Player 2: (3, 2)
Player 1: (1, 3) # Game ends, P1 wins
Execution Video
How to Play Tic Tac Toe with Appium
The board is made up of 9 cells (3x3). To play, we need to find and tap a specific cell.
Each cell does not have a unique identifier. However, all cells have the type XCUIElementTypeImage
(getting this info via the Appium Inspector). I used driver.find_elements
on the type to get all 9 cells. Then indexing to get a specific cell.
board = driver.find_elements(:class_name, "XCUIElementTypeImage")
# Board layout
# 0 1 2
# 3 4 5
# 6 7 8
board[0].click # taps the top-left cell
Using this, it’s simple to write a test case.
it "Play Tic Tac Toe - Player x Player - X Wins" do
driver.find_element(:name, "Player VS Player").click
board = driver.find_elements(:class_name, "XCUIElementTypeImage")
# 0 1 2
# 3 4 5
# 6 7 8
board[0].click
board[6].click
board[1].click
board[7].click
board[2].click
game_status_text = driver.find_element(:class_name, "XCUIElementTypeStaticText")["value"]
expect(game_status_text).to eq("Game has ended! Winner: Player X")
end
Using indexing is not very intuitive though, we can do better.
Refactoring
To make the script more readable, let’s use coordinates. While we are at it, we can refactor based on Maintainable Automated Test Design and use the Page Object Models design here as well.
First, I select driver.find_element(:name, "Player VS Player").click
, then invoke the ‘Extract to Page Function’ refactoring in TestWise.
Then enter page and function name.
Apply the refactoring to get:
menu_page = MenuPage.new(driver)
menu_page.click_player_vs_player_mode
Then, convert coordinates to indexes with a simple calculation.
class GameBoardPage < AbstractPage
def initialize(driver)
super(driver, "")
end
# (1, 1) is top-left, (3, 3) is bottom-right
def tap(x, y)
board = driver.find_elements(:class_name, "XCUIElementTypeImage")
index = (x - 1) * 3 + (y - 1)
board[index].click
end
end
The refactored test looks like:
it "Play Tic Tac Toe - Player x Player - X Wins" do
menu_page = MenuPage.new(driver)
menu_page.click_player_vs_player_mode
game_board_page = GameBoardPage.new(driver)
game_board_page.tap(1, 1)
game_board_page.tap(3, 1)
game_board_page.tap(1, 2)
game_board_page.tap(3, 2)
game_board_page.tap(1, 3)
expect(game_board_page.game_status_text).to eq("Game has ended! Winner: Player X")
end
Doesn’t it look much nicer?
Complete Test Scripts
menu_page.rb
class MenuPage < AbstractPage
def initialize(driver)
super(driver, "") # <= WIN TITLE
end
def click_player_vs_player_mode
driver.find_element(:name, "Player VS Player").click
end
end
game_board_page.rb
class GameBoardPage < AbstractPage
def initialize(driver)
super(driver, "")
end
def tap(x, y)
board = driver.find_elements(:class_name, "XCUIElementTypeImage")
index = (x - 1) * 3 + (y - 1)
board[index].click
end
def game_status_text
driver.find_element(:class_name, "XCUIElementTypeStaticText")["value"]
end
end
player_vs_player_spec.rb
load File.dirname(__FILE__) + "/../test_helper.rb"
describe "Player Vs Player Games" do
include TestHelper
before(:all) do
@driver = Appium::Driver.new(appium_opts).start_driver
end
after(:all) do
driver.quit unless debugging?
end
it "Play Tic Tac Toe - Player x Player - X Wins" do
menu_page = MenuPage.new(driver)
menu_page.click_player_vs_player_mode
game_board_page = GameBoardPage.new(driver)
game_board_page.tap(1, 1)
game_board_page.tap(3, 1)
game_board_page.tap(1, 2)
game_board_page.tap(3, 2)
game_board_page.tap(1, 3)
expect(game_board_page.game_status_text).to eq("Game has ended! Winner: Player X")
end
end