
移动端自动化测试,不是新鲜事。但90%的测试工程师,还在手动点点点。
为什么?因为他们害怕技术门槛。Android用Java,iOS用Swift,两套代码,两套框架,一个人学两门语言?想想就头大。
Appium解决了这个痛点。它用一套API,搞定双平台。
Appium是基于WebDriver协议的扩展。Selenium能做的事,Appium基本都能做。区别在于:Selenium操作浏览器,Appium操作手机。
一个协议,两个平台,三行代码,零门槛。
第一步:安装Node.js
Appium Server基于Node.js运行。
第二步:安装Appium
第三步:安装Python客户端库
第四步:配置Android SDK
第五步:配置Xcode(仅iOS需要)
环境搭建不是技术活,是耐心活。3小时搞定,效率提升90%。
环境搭好了,现在开始写代码。别怕,我带你一行一行敲。
Android设备连接:
iOS设备连接:
Desired Capabilities是Appium的核心配置项,告诉Appium要测试什么设备、什么应用。
配置写不对,测试两行泪。Desired Capabilities是测试的灵魂。
Appium支持8种定位方式,和Selenium完全一致。
import unittest
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
import time
class CalculatorTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""测试前准备"""
desired_caps = {
"platformName": "Android",
"platformVersion": "12.0",
"deviceName": "emulator-5554",
"appPackage": "com.android.calculator2",
"appActivity": ".Calculator",
"noReset": True,
"unicodeKeyboard": True,
"resetKeyboard": True
}
cls.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
cls.driver.implicitly_wait(10)
def test_addition(self):
"""测试加法运算"""
digit_5 = self.driver.find_element(AppiumBy.ID, "com.android.calculator2:id/digit_5")
digit_5.click()
plus = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "plus")
plus.click()
digit_3 = self.driver.find_element(AppiumBy.ID, "com.android.calculator2:id/digit_3")
digit_3.click()
equals = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "equals")
equals.click()
result = self.driver.find_element(AppiumBy.ID, "com.android.calculator2:id/result")
self.assertEqual(result.text, "8")
print(f"5 + 3 = {result.text} ✓")
def test_subtraction(self):
"""测试减法运算"""
digit_9 = self.driver.find_element(AppiumBy.ID, "com.android.calculator2:id/digit_9")
digit_9.click()
minus = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "minus")
minus.click()
digit_4 = self.driver.find_element(AppiumBy.ID, "com.android.calculator2:id/digit_4")
digit_4.click()
equals = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "equals")
equals.click()
result = self.driver.find_element(AppiumBy.ID, "com.android.calculator2:id/result")
self.assertEqual(result.text, "5")
print(f"9 - 4 = {result.text} ✓")
@classmethod
def tearDownClass(cls):
"""测试后清理"""
time.sleep(3)
cls.driver.quit()
if __name__ == "__main__":
unittest.main()
import os
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
import unittest
class BaseTest:
"""测试基类,封装双平台差异"""
def get_desired_caps(self):
"""根据环境变量获取配置"""
platform = os.getenv("PLATFORM", "Android")
if platform == "Android":
return {
"platformName": "Android",
"platformVersion": "12.0",
"deviceName": "emulator-5554",
"appPackage": "com.example.app",
"appActivity": ".MainActivity",
"noReset": True,
"automationName": "UiAutomator2"
}
else:
return {
"platformName": "iOS",
"platformVersion": "15.0",
"deviceName": "iPhone 13 Pro",
"app": "/path/to/app.app",
"noReset": True,
"automationName": "XCUITest"
}
def find_element_by_text(self, text):
"""跨平台文本定位"""
platform = os.getenv("PLATFORM", "Android")
if platform == "Android":
return self.driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,
f'new UiSelector().text("{text}")')
else:
return self.driver.find_element(AppiumBy.IOS_PREDICATE,
f'label == "{text}"')
class ScrollHelper:
"""滑动操作工具类"""
@staticmethod
def swipe_up(driver, duration=800):
"""向上滑动"""
size = driver.get_window_size()
start_x = size['width'] // 2
start_y = size['height'] * 3 // 4
end_y = size['height'] // 4
driver.swipe(start_x, start_y, start_x, end_y, duration)
@staticmethod
def swipe_down(driver, duration=800):
"""向下滑动"""
size = driver.get_window_size()
start_x = size['width'] // 2
start_y = size['height'] // 4
end_y = size['height'] * 3 // 4
driver.swipe(start_x, start_y, start_x, end_y, duration)
@staticmethod
def swipe_left(driver, duration=800):
"""向左滑动"""
size = driver.get_window_size()
start_x = size['width'] * 3 // 4
start_y = size['height'] // 2
end_x = size['width'] // 4
driver.swipe(start_x, start_y, end_x, start_y, duration)
@staticmethod
def swipe_to_element(driver, element, max_swipes=5):
"""滑动直到元素可见"""
for i in range(max_swipes):
try:
if element.is_displayed():
return True
except:
ScrollHelper.swipe_up(driver)
return False
class LoginTest(BaseTest, unittest.TestCase):
@classmethod
def setUpClass(cls):
caps = cls.get_desired_caps(cls)
cls.driver = webdriver.Remote('http://localhost:4723/wd/hub', caps)
cls.driver.implicitly_wait(10)
def test_successful_login(self):
"""测试成功登录"""
username_field = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "username")
username_field.send_keys("test_user")
password_field = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "password")
password_field.send_keys("password123")
login_button = self.find_element_by_text("登录")
login_button.click()
welcome_text = self.find_element_by_text("欢迎回来")
self.assertTrue(welcome_text.is_displayed())
print("登录测试通过 ✓")
def test_failed_login(self):
"""测试登录失败"""
username_field = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "username")
username_field.send_keys("test_user")
password_field = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "password")
password_field.send_keys("wrong_password")
login_button = self.find_element_by_text("登录")
login_button.click()
error_message = self.find_element_by_text("用户名或密码错误")
self.assertTrue(error_message.is_displayed())
print("登录失败测试通过 ✓")
@classmethod
def tearDownClass(cls):
cls.driver.quit()
// Jenkinsfile
pipeline {
agent any
environment {
PLATFORM = 'Android'
APPIUM_PORT = '4723'
APPIUM_HOST = 'localhost'
}
stages {
stage('Setup Environment') {
steps {
script {
// 启动Appium Server
sh 'nohup appium --port 4723 &'
sleep 5
// 检查设备连接
sh 'adb devices'
}
}
}
stage('Run Tests') {
parallel {
stage('Android Tests') {
environment {
PLATFORM = 'Android'
}
steps {
script {
sh 'python -m pytest android_tests/ --junitxml=android_results.xml'
}
}
}
stage('iOS Tests') {
environment {
PLATFORM = 'iOS'
}
steps {
script {
sh 'python -m pytest ios_tests/ --junitxml=ios_results.xml'
}
}
}
}
}
stage('Report Generation') {
steps {
script {
// 生成Allure报告
sh 'allure generate --clean allure-results'
}
}
}
}
post {
always {
// 清理环境
script {
sh 'pkill appium'
}
}
success {
echo '所有测试通过!'
}
failure {
echo '测试失败,请检查日志'
}
}
}
FROM python:3.9-slim
RUN apt-get update && \
apt-get install -y nodejs npm && \
npm install -g appium
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
ENV ANDROID_SDK_ROOT=/opt/android-sdk
ENV PATH=$PATH:$ANDROID_SDK_ROOT/platform-tools
COPY . /app
WORKDIR /app
CMD ["python", "-m", "pytest", "tests/"]
import allure
import pytest
from appium import webdriver
@allure.feature("登录功能")
class TestLogin:
@allure.story("成功登录")
@allure.severity(allure.severity_level.CRITICAL)
def test_successful_login(self):
"""测试成功登录场景"""
with allure.step("输入用户名"):
username_field = self.driver.find_element(...)
username_field.send_keys("test_user")
with allure.step("输入密码"):
password_field = self.driver.find_element(...)
password_field.send_keys("password123")
with allure.step("点击登录按钮"):
login_button = self.driver.find_element(...)
login_button.click()
with allure.step("验证登录结果"):
result = self.driver.find_element(...)
assert result.text == "登录成功"
allure.attach(self.driver.get_screenshot_as_png(),
name="登录成功截图",
attachment_type=allure.attachment_type.PNG)