在当今移动互联网时代,手机应用已经成为人们日常生活中不可或缺的一部分。随着移动应用的快速发展,自动化测试和手机操作的需求也日益增长。Appium作为一个开源的移动应用自动化测试框架,能够帮助我们实现这一目标。
Appium支持多种编程语言,包括Python、Java、Ruby等,并可以同时测试Android和iOS平台的应用。它采用WebDriver协议,使得我们可以使用熟悉的Selenium WebDriver API来编写移动应用的自动化脚本。
本文将详细介绍如何使用Python和Appium来操作手机,从环境搭建到实际脚本编写,帮助读者快速掌握这一实用技能。
在开始使用Appium之前,我们需要完成一系列的环境配置工作。以下是详细的步骤:
Appium服务器是基于Node.js开发的,因此首先需要安装Node.js。可以从Node.js官网下载并安装最新版本。
安装完成后,可以通过以下命令验证安装是否成功:
|
1 2 |
node --version npm --version |
通过NPM全局安装Appium:
|
1 |
npm install -g appium |
安装完成后,可以通过以下命令启动Appium服务器:
|
1 |
appium |
使用pip安装Appium的Python客户端库:
|
1 |
pip install Appium-Python-Client |
对于Android设备,需要安装Android SDK并配置环境变量:
Appium需要Java环境支持,请安装JDK 8或更高版本。
Appium采用客户端-服务器架构:
Appium服务器:接收来自客户端的命令,并将其转换为移动设备可以理解的原生命令
Appium客户端:各种编程语言的客户端库,用于发送命令到Appium服务器

Desired Capabilities是一组键值对,用于告诉Appium服务器我们想要启动怎样的会话。常见的Capabilities包括:
Appium支持多种元素定位策略:
下面我们将编写一个简单的Appium脚本,用于打开手机上的计算器应用并进行简单的计算操作。
|
1 2 3 4 5 |
from appium import webdriver from appium.webdriver.common.appiumby import AppiumBy from appium.options.android import UiAutomator2Options import time import unittest |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
def setup_driver(): # 配置Appium选项 options = UiAutomator2Options()
# 设置设备基本信息 options.platform_name = 'Android' options.platform_version = '12' # 根据你的设备版本修改 options.device_name = 'Android Emulator' options.automation_name = 'UiAutomator2'
# 设置应用信息 options.app_package = 'com.android.calculator2' options.app_activity = 'com.android.calculator2.Calculator'
# 其他设置 options.no_reset = True # 不重置应用状态
# 连接Appium服务器 driver = webdriver.Remote('http://localhost:4723', options=options)
return driver |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
def calculator_operations(driver): """执行计算器操作""" try: # 等待计算器加载完成 time.sleep(2)
# 定位数字按钮和操作符 # 注意:不同设备上的计算器应用可能具有不同的元素ID # 这里使用的是Android原生计算器的元素ID
# 点击数字 7 btn_7 = driver.find_element(AppiumBy.ID, 'com.android.calculator2:id/digit_7') btn_7.click()
# 点击加号 btn_plus = driver.find_element(AppiumBy.ID, 'com.android.calculator2:id/op_add') btn_plus.click()
# 点击数字 8 btn_8 = driver.find_element(AppiumBy.ID, 'com.android.calculator2:id/digit_8') btn_8.click()
# 点击等号 btn_equals = driver.find_element(AppiumBy.ID, 'com.android.calculator2:id/eq') btn_equals.click()
# 获取结果 result = driver.find_element(AppiumBy.ID, 'com.android.calculator2:id/result') calculated_result = result.text
print(f"计算结果: 7 + 8 = {calculated_result}")
# 验证结果是否正确 expected_result = '15' if calculated_result == expected_result: print("测试通过!计算结果正确。") else: print(f"测试失败!期望结果: {expected_result}, 实际结果: {calculated_result}")
return calculated_result
except Exception as e: print(f"操作过程中出现错误: {str(e)}") return None |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
class CalculatorTest(unittest.TestCase): """计算器测试类"""
def setUp(self): """测试前置设置""" self.driver = setup_driver() self.driver.implicitly_wait(10) # 设置隐式等待时间
def tearDown(self): """测试后置清理""" if self.driver: self.driver.quit()
def test_addition_operation(self): """测试加法运算""" result = calculator_operations(self.driver) self.assertEqual(result, '15', "加法运算结果不正确")
def test_clear_operation(self): """测试清除操作""" try: # 先输入一些数字 btn_5 = self.driver.find_element(AppiumBy.ID, 'com.android.calculator2:id/digit_5') btn_5.click()
# 点击清除按钮 btn_clear = self.driver.find_element(AppiumBy.ID, 'com.android.calculator2:id/clr') btn_clear.click()
# 验证显示区域是否已清除 result = self.driver.find_element(AppiumBy.ID, 'com.android.calculator2:id/formula') current_display = result.text
# 清除后显示区域应该为空或显示0 self.assertTrue(not current_display or current_display == '0', "清除操作未正常工作")
except Exception as e: self.fail(f"清除操作测试失败: {str(e)}") |
在实际应用中,我们可能会遇到各种复杂的定位情况。以下是一些常用的定位方法:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
def advanced_element_locating(driver): """演示高级元素定位方法"""
# 1. 使用XPath定位 # 通过文本内容定位元素 element_by_text = driver.find_element(AppiumBy.XPATH, "//*[@text='确定']")
# 通过部分文本内容定位 element_by_partial_text = driver.find_element( AppiumBy.XPATH, "//*[contains(@text, '确定')]" )
# 2. 使用Accessibility ID定位(通常对应content-desc属性) element_by_accessibility = driver.find_element( AppiumBy.ACCESSIBILITY_ID, "按钮描述" )
# 3. 使用Class Name定位 elements_by_class = driver.find_elements( AppiumBy.CLASS_NAME, "android.widget.Button" )
# 4. 使用Android UI Automator定位 element_by_uiautomator = driver.find_element( AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("确定")' )
return { 'by_text': element_by_text, 'by_partial_text': element_by_partial_text, 'by_accessibility': element_by_accessibility, 'by_class': elements_by_class, 'by_uiautomator': element_by_uiautomator } |
Appium支持多种手势操作,如滑动、长按、拖拽等:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
def gesture_operations(driver): """演示手势操作"""
# 获取屏幕尺寸 window_size = driver.get_window_size() screen_width = window_size['width'] screen_height = window_size['height']
# 1. 滑动操作 - 从底部滑动到顶部 start_x = screen_width / 2 start_y = screen_height * 0.8 end_x = screen_width / 2 end_y = screen_height * 0.2
driver.swipe(start_x, start_y, end_x, end_y, 1000)
# 2. 滚动操作 # 滚动到指定元素 scroll_to_element = driver.find_element( AppiumBy.ANDROID_UIAUTOMATOR, 'new UiScrollable(new UiSelector().scrollable(true))' '.scrollIntoView(new UiSelector().text("目标元素"))' )
# 3. 长按操作 element_to_long_press = driver.find_element(AppiumBy.ID, 'some.element.id') driver.long_press(element_to_long_press)
# 4. 拖拽操作 source_element = driver.find_element(AppiumBy.ID, 'source.element') target_element = driver.find_element(AppiumBy.ID, 'target.element') driver.drag_and_drop(source_element, target_element) |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
def handle_popups_and_permissions(driver): """处理弹窗和权限请求"""
try: # 尝试查找并点击允许按钮 allow_button = driver.find_element( AppiumBy.ID, 'com.android.packageinstaller:id/permission_allow_button' ) allow_button.click() print("已处理权限请求")
except Exception: # 如果找不到特定的允许按钮,尝试其他方式 try: # 使用文本定位允许按钮 allow_by_text = driver.find_element( AppiumBy.XPATH, "//*[@text='允许' or @text='ALLOW']" ) allow_by_text.click() print("通过文本定位处理了权限请求")
except Exception: print("未找到权限请求弹窗或处理失败")
# 处理其他类型的弹窗 try: # 查找确定、好的、知道了等按钮 confirm_buttons = [ "确定", "确认", "好的", "知道了", "OK", "Okay" ]
for button_text in confirm_buttons: try: confirm_btn = driver.find_element( AppiumBy.XPATH, f"//*[@text='{button_text}']" ) confirm_btn.click() print(f"点击了 {button_text} 按钮") break except Exception: continue
except Exception as e: print(f"处理弹窗时出现错误: {str(e)}") |
合理的等待策略对于自动化测试的稳定性至关重要:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC
def wait_strategies(driver): """演示不同的等待策略"""
# 1. 显式等待 - 等待元素可点击 wait = WebDriverWait(driver, 10) element = wait.until( EC.element_to_be_clickable((AppiumBy.ID, 'some.element.id')) ) element.click()
# 2. 显式等待 - 等待元素可见 visible_element = wait.until( EC.visibility_of_element_located((AppiumBy.ID, 'visible.element')) )
# 3. 显式等待 - 等待元素存在(不一定可见) present_element = wait.until( EC.presence_of_element_located((AppiumBy.ID, 'present.element')) )
# 4. 自定义等待条件 def custom_condition(driver): """自定义等待条件""" try: element = driver.find_element(AppiumBy.ID, 'custom.element') return element.is_displayed() except Exception: return False
custom_element = wait.until(custom_condition)
return { 'clickable': element, 'visible': visible_element, 'present': present_element, 'custom': custom_element } |
下面是一个完整的Appium脚本示例,展示了如何使用Python操作手机:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 |
#!/usr/bin/env python3 """ Appium手机操作示例 作者:你的名字 日期:2024年1月 描述:使用Python和Appium操作手机计算器应用 """
import time import unittest import logging from appium import webdriver from appium.webdriver.common.appiumby import AppiumBy from appium.options.android import UiAutomator2Options from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException, NoSuchElementException
# 配置日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__)
class MobileAutomationFramework: """移动自动化框架类"""
def __init__(self, server_url='http://localhost:4723'): self.server_url = server_url self.driver = None self.wait = None
def setup_driver(self, capabilities_dict=None): """设置Appium驱动"""
if capabilities_dict is None: # 默认配置 - Android计算器 capabilities_dict = { 'platformName': 'Android', 'platformVersion': '12', 'deviceName': 'Android Emulator', 'automationName': 'UiAutomator2', 'appPackage': 'com.android.calculator2', 'appActivity': 'com.android.calculator2.Calculator', 'noReset': True, 'newCommandTimeout': 300 }
try: options = UiAutomator2Options() for key, value in capabilities_dict.items(): setattr(options, key, value)
logger.info("正在连接Appium服务器...") self.driver = webdriver.Remote(self.server_url, options=options)
# 设置显式等待 self.wait = WebDriverWait(self.driver, 15)
logger.info("Appium驱动设置成功") return True
except Exception as e: logger.error(f"设置Appium驱动失败: {str(e)}") return False
def teardown(self): """清理资源""" if self.driver: self.driver.quit() logger.info("Appium驱动已关闭")
def find_element_with_wait(self, by, value, timeout=15): """带等待的元素查找""" wait = WebDriverWait(self.driver, timeout) return wait.until(EC.presence_of_element_located((by, value)))
def safe_click(self, by, value, timeout=15): """安全的点击操作""" try: element = self.find_element_with_wait(by, value, timeout) element.click() logger.info(f"成功点击元素: {value}") return True except Exception as e: logger.error(f"点击元素失败: {value}, 错误: {str(e)}") return False
def take_screenshot(self, filename=None): """截取屏幕截图""" if filename is None: filename = f"screenshot_{int(time.time())}.png"
try: self.driver.save_screenshot(filename) logger.info(f"截图已保存: {filename}") return True except Exception as e: logger.error(f"截图失败: {str(e)}") return False
def get_page_source(self): """获取页面源代码""" try: return self.driver.page_source except Exception as e: logger.error(f"获取页面源代码失败: {str(e)}") return None
class CalculatorAutomation(MobileAutomationFramework): """计算器自动化类"""
def __init__(self): super().__init__() self.number_mapping = { '0': 'digit_0', '1': 'digit_1', '2': 'digit_2', '3': 'digit_3', '4': 'digit_4', '5': 'digit_5', '6': 'digit_6', '7': 'digit_7', '8': 'digit_8', '9': 'digit_9' } self.operator_mapping = { '+': 'op_add', '-': 'op_sub', '*': 'op_mul', '/': 'op_div' }
def input_number(self, number): """输入数字""" if not isinstance(number, (int, str)): raise ValueError("数字必须是整数或字符串")
number_str = str(number) for digit in number_str: if digit in self.number_mapping: element_id = f"com.android.calculator2:id/{self.number_mapping[digit]}" self.safe_click(AppiumBy.ID, element_id) time.sleep(0.1) # 短暂延迟,确保输入稳定 else: logger.warning(f"无法识别的数字: {digit}")
def input_operator(self, operator): """输入操作符""" if operator not in self.operator_mapping: raise ValueError(f"不支持的操作符: {operator}")
element_id = f"com.android.calculator2:id/{self.operator_mapping[operator]}" self.safe_click(AppiumBy.ID, element_id)
def calculate(self, num1, operator, num2): """执行计算""" logger.info(f"执行计算: {num1} {operator} {num2}")
# 输入第一个数字 self.input_number(num1)
# 输入操作符 self.input_operator(operator)
# 输入第二个数字 self.input_number(num2)
# 点击等号 self.safe_click(AppiumBy.ID, 'com.android.calculator2:id/eq')
# 获取结果 return self.get_result()
def get_result(self): """获取计算结果""" try: result_element = self.find_element_with_wait( AppiumBy.ID, 'com.android.calculator2:id/result' ) result = result_element.text logger.info(f"计算结果: {result}") return result except Exception as e: logger.error(f"获取结果失败: {str(e)}") return None
def clear_calculator(self): """清除计算器""" self.safe_click(AppiumBy.ID, 'com.android.calculator2:id/clr') logger.info("计算器已清除")
class TestCalculatorOperations(unittest.TestCase): """计算器操作测试类"""
@classmethod def setUpClass(cls): """测试类设置""" cls.calculator = CalculatorAutomation() success = cls.calculator.setup_driver() if not success: raise Exception("无法初始化Appium驱动")
@classmethod def tearDownClass(cls): """测试类清理""" cls.calculator.teardown()
def setUp(self): """单个测试设置""" # 确保每次测试前计算器是清除状态 self.calculator.clear_calculator() time.sleep(1)
def test_addition(self): """测试加法""" result = self.calculator.calculate(15, '+', 7) self.assertEqual(result, '22', "加法测试失败")
def test_subtraction(self): """测试减法""" result = self.calculator.calculate(20, '-', 8) self.assertEqual(result, '12', "减法测试失败")
def test_multiplication(self): """测试乘法""" result = self.calculator.calculate(6, '*', 9) self.assertEqual(result, '54', "乘法测试失败")
def test_division(self): """测试除法""" result = self.calculator.calculate(56, '/', 7) self.assertEqual(result, '8', "除法测试失败")
def test_complex_operation(self): """测试复杂运算""" # 15 + 27 - 8 self.calculator.input_number(15) self.calculator.input_operator('+') self.calculator.input_number(27) self.calculator.input_operator('-') self.calculator.input_number(8) self.calculator.safe_click(AppiumBy.ID, 'com.android.calculator2:id/eq')
result = self.calculator.get_result() self.assertEqual(result, '34', "复杂运算测试失败")
def main(): """主函数""" logger.info("开始Appium手机操作演示")
# 创建计算器自动化实例 calculator = CalculatorAutomation()
try: # 设置驱动 if not calculator.setup_driver(): logger.error("无法启动Appium驱动,程序退出") return
# 执行一系列测试计算 test_calculations = [ (8, '+', 4), (15, '-', 6), (7, '*', 9), (81, '/', 9) ]
for num1, op, num2 in test_calculations: result = calculator.calculate(num1, op, num2) expected = str(eval(f"{num1}{op}{num2}"))
if result == expected: logger.info(f"? {num1} {op} {num2} = {result} (正确)") else: logger.error(f"? {num1} {op} {num2} = {result} (期望: {expected})")
# 截取屏幕截图 calculator.take_screenshot("calculator_final_state.png")
logger.info("Appium手机操作演示完成")
except Exception as e: logger.error(f"程序执行过程中出现错误: {str(e)}")
finally: # 确保资源被正确清理 calculator.teardown()
if __name__ == "__main__": # 可以直接运行演示 main()
# 或者运行单元测试 # unittest.main(verbosity=2) |
问题:无法连接到Appium服务器
解决方案:
问题:脚本无法找到指定元素
解决方案:
问题:应用权限请求导致脚本中断
解决方案:
问题:脚本运行缓慢
解决方案:
使用Page Object模式将页面元素和操作封装成类
将配置信息与测试逻辑分离
使用配置文件或环境变量管理设备信息和应用信息
实现完善的异常处理机制
添加重试机制处理偶发性失败
使用日志记录详细的操作信息
使用有意义的变量名和函数名
添加清晰的注释和文档
定期更新Appium和相关依赖
通过本文的介绍,我们学习了如何使用Python和Appium来操作手机应用。从环境搭建到基础操作,再到高级技巧,我们覆盖了使用Appium进行移动自动化的关键知识点。
Appium作为一个强大的跨平台移动自动化工具,结合Python的简洁语法,为我们提供了强大的手机操作能力。无论是进行自动化测试还是实现复杂的手机操作流程,Appium都是一个值得掌握的技能。
随着移动应用的不断发展,掌握移动自动化技术将会变得越来越重要。希望本文能够为你提供一个良好的起点,帮助你在移动自动化的道路上走得更远。
注意:在实际使用中,请根据你的具体设备和应用调整代码中的元素定位信息和配置参数。不同的设备和应用版本可能会有差异,需要灵活调整脚本。