# Python自动化测试面试题

🔗测试开发训练营 (opens new window):针对面试求职的测试开发训练营,大厂导师 1v1 私教,帮助学员从 0 到 1 拿到测开满意的 offer

  • ✅直播授课+实时互动答疑,课程到就业贯穿企业级案例,由测试开发导师全程主讲,绝无其他助理老师代课。

  • ✅大厂测试开发导师一对一求职陪跑,覆盖课程答疑+简历打磨+面试模拟面试+面试复盘等求职辅导

# 一、接口自动化(高优)

# 1. 你在项目中使用的接口自动化测试框架是什么?为什么选择它?

回答话术:

我使用的是 Requests + Pytest + Allure 组合框架。选择它主要基于三个原因:

首先,Requests库非常简洁易用,支持所有HTTP方法,能自动处理Session和Cookie,满足大部分接口测试需求。

其次,Pytest提供了强大的fixture机制和参数化功能,可以很好地管理测试数据和前置后置条件,而且失败重跑、并发执行这些功能都有现成的插件支持,非常适合做CI/CD集成。

最后,Allure能生成非常直观的HTML测试报告,支持用例分类、历史趋势分析,方便向团队展示测试结果。

这套组合既满足了功能测试需求,学习成本也不高,团队成员上手很快。

# 2. 请描述你设计的接口自动化测试框架的整体架构

回答话术:

我设计的框架采用分层架构,主要分为五层:

第一层是API层,负责封装所有接口请求,按业务模块划分文件,比如user_api.py、order_api.py,每个接口对应一个方法。

第二层是测试用例层,这里只关注测试逻辑,调用API层的方法,不涉及具体的请求细节。

第三层是公共工具层,封装了请求发送、日志记录、断言验证等通用功能,避免重复代码。

第四层是配置层,使用YAML文件管理不同环境的配置,支持开发、测试、生产环境快速切换。

第五层是数据层,测试数据独立管理,支持YAML、JSON、Excel等多种格式。

这样设计的好处是职责清晰、易于维护,修改接口只需要改API层,修改测试数据不影响用例代码。

# 3. 如何实现接口请求的二次封装?

回答话术:

我会创建一个RequestUtil工具类,对Requests库进行二次封装。主要实现四个功能:

第一是统一请求入口,封装get、post、put、delete等方法,所有请求都走这个工具类。

第二是自动记录日志,在发送请求前后自动打印请求URL、参数、响应结果,方便定位问题。

第三是统一异常处理,捕获超时、连接异常等错误,统一处理并记录。

第四是Session管理,使用Session对象自动管理Cookie,支持全局设置token。

class RequestUtil:
    def send_request(self, method, url, **kwargs):
        logger.info(f"请求: {method} {url}")
        response = self.session.request(method, url, **kwargs)
        logger.info(f"响应: {response.status_code}")
        return response

这样封装后,测试用例中只需要关注业务逻辑,不用每次都写日志和异常处理代码。

# 4. 如何设计API层实现接口的统一管理?

回答话术:

我会先创建一个BaseApi基类,里面封装公共的请求方法。然后针对每个业务模块创建独立的API类,继承BaseApi。

比如UserApi类专门管理用户相关接口,每个接口封装成一个方法,方法名语义化,参数清晰。

class UserApi(BaseApi):
    def login(self, username, password):
        return self.send('POST', '/api/login', 
                        json={'username': username, 'password': password})
    
    def get_user_info(self, user_id):
        return self.send('GET', f'/api/users/{user_id}')

这样设计的优点是:接口按模块分类很清晰,调用时语义明确,而且如果接口有变化,只需要修改对应的API类,测试用例不受影响。

# 5. 如何实现多环境配置管理?

回答话术:

我使用YAML文件管理配置,在config.yaml中定义dev、test、prod三个环境的配置,包括接口域名、数据库连接信息等。

然后创建Config类读取配置,支持通过环境变量或配置文件切换环境。

env: test
test:
  base_url: http://test-api.example.com
  db:
    host: test-db.example.com

切换环境有两种方式:一是修改配置文件中的env值,二是通过环境变量export TEST_ENV=dev来指定。

这样做的好处是配置集中管理,切换环境很方便,而且不用在代码里硬编码环境信息。

# 6. 如何实现测试数据的参数化?

回答话术:

我主要用两种方式管理测试数据:

第一种是使用Pytest的parametrize装饰器,适合数据量少的场景,直接在用例上标注。

@pytest.mark.parametrize("username,password,expected", [
    ("admin", "123456", 0),
    ("admin", "wrong", 1001),
])
def test_login(self, username, password, expected):
    response = self.user_api.login(username, password)
    assert response.json()['code'] == expected

第二种是外部数据文件,用YAML或Excel存储测试数据,通过工具类读取后传给parametrize。适合数据量大或需要非技术人员维护数据的场景。

这样实现数据驱动测试,同一个用例可以跑多组数据,代码复用率高。

# 7. 如何实现统一的断言机制?

回答话术:

我封装了一个AssertUtil工具类,提供常用的断言方法:

class AssertUtil:
    @staticmethod
    def assert_code(response, expected_code):
        assert response.status_code == expected_code
    
    @staticmethod
    def assert_json_value(response, json_path, expected_value):
        actual = jsonpath(response.json(), json_path)[0]
        assert actual == expected_value

主要提供四类断言:状态码断言、JSON字段值断言、响应时间断言、字段存在性断言。

每个断言方法都会自动记录日志,失败时输出详细的对比信息,方便定位问题。

这样做的好处是断言逻辑统一,输出格式一致,而且可以在断言方法里加入更多增强功能,比如失败截图、失败重试等。

# 8. Pytest的fixture在框架中如何应用?

回答话术:

我主要用fixture实现三类功能:

第一是环境准备,比如用session级别的fixture获取登录token,整个测试会话只登录一次,所有用例共享。

第二是数据准备和清理,用yield实现,测试前创建数据,测试后自动删除,保证环境干净。

@pytest.fixture
def create_user(self):
    user_id = self.user_api.create_user()
    yield user_id
    self.user_api.delete_user(user_id)  # 自动清理

第三是依赖注入,fixture可以相互依赖,比如auth_headers依赖login_token,自动组装测试所需的数据。

通过不同的scope控制fixture的生命周期,function、class、module、session灵活选择。

# 9. 如何在框架中实现日志管理?

回答话术:

我使用Python的logging模块实现日志管理,封装了一个Logger工具类。

主要实现四个功能:

第一是分级输出,设置DEBUG、INFO、ERROR等不同级别,开发时用DEBUG,正式运行用INFO。

第二是双重输出,日志同时输出到控制台和文件,文件按日期自动分割。

第三是格式统一,日志包含时间、级别、文件名、行号、具体内容,便于追溯。

formatter = '%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'

第四是与Allure集成,把日志附加到测试报告中,失败时可以直接在报告里查看日志。

这样做可以快速定位问题,特别是CI/CD环境中看不到控制台输出时,日志文件非常重要。

# 10. 如何处理接口的依赖关系?

回答话术:

接口依赖主要有两类:

第一类是token依赖,很多接口需要先登录获取token。我用fixture实现,在conftest.py中创建login_token的fixture,需要认证的用例直接引用这个fixture。

第二类是数据依赖,比如测试订单详情接口需要先创建订单。我也用fixture实现,用yield保证数据用完后清理。

@pytest.fixture
def order_id(self):
    # 创建订单
    response = self.order_api.create_order()
    order_id = response.json()['data']['id']
    yield order_id
    # 测试完成后删除订单
    self.order_api.delete_order(order_id)

复杂场景下,我会在API类中封装组合接口方法,把多个有依赖关系的接口调用封装成一个方法,用例直接调用。

# 11. 如何实现接口测试报告?

回答话术:

我使用Allure生成测试报告,主要包含五个步骤:

第一步安装插件:pytest-allure-adaptor

第二步在用例中添加装饰器,标注用例的功能模块、优先级、标签等。

@allure.feature("用户模块")
@allure.story("用户登录")
@allure.severity("blocker")
def test_login(self):
    pass

第三步在断言或关键步骤添加日志,使用allure.attach添加截图、请求响应等附件。

第四步执行用例时生成JSON数据pytest --alluredir=./report

第五步生成HTML报告allure generate ./report -o ./html --clean

报告中可以看到用例执行情况、成功率、失败原因、历史趋势等,非常直观,适合向项目组展示。

# 12. 如何实现接口的数据库校验?

回答话术:

我封装了一个DBUtil工具类,使用PyMySQL或SQLAlchemy连接数据库。

典型使用场景是:调用创建订单接口后,查询数据库验证订单记录是否正确插入。

class DBUtil:
    def query(self, sql):
        cursor.execute(sql)
        return cursor.fetchall()

# 使用示例
def test_create_order(self):
    response = self.order_api.create_order()
    order_id = response.json()['data']['order_id']
    
    # 数据库校验
    sql = f"SELECT * FROM orders WHERE id={order_id}"
    result = self.db.query(sql)
    assert result[0]['status'] == 'created'

需要注意的是,数据库连接信息从配置文件读取,不同环境连接不同数据库。而且用完要及时关闭连接,避免连接泄露。

# 13. 如何处理接口的加密和签名?

回答话术:

我遇到过两种常见场景:

第一种是参数签名,比如MD5签名。我会在BaseApi中封装generate_sign方法,按照约定的规则(参数排序、拼接、加密)生成签名,发送请求前自动添加。

def generate_sign(self, params):
    sorted_str = '&'.join([f"{k}={v}" for k, v in sorted(params.items())])
    return hashlib.md5((sorted_str + SECRET_KEY).encode()).hexdigest()

第二种是响应解密,比如AES加密。在RequestUtil的send_request方法中,收到响应后先判断是否加密,如果加密就先解密再返回。

我会把加解密逻辑封装成独立的工具类,这样修改加密算法时不影响其他代码。

# 14. 如何实现接口的Mock测试?

回答话术:

我主要用两种方式实现Mock:

第一种是使用responses库,拦截Requests请求,返回预设的响应数据,适合测试环境不稳定或依赖的第三方接口还没开发完成的情况。

import responses

@responses.activate
def test_with_mock(self):
    responses.add(responses.GET, 'http://api.example.com/user',
                 json={'code': 0, 'data': {'name': 'test'}}, status=200)
    
    response = requests.get('http://api.example.com/user')
    assert response.json()['code'] == 0

第二种是搭建Mock Server,使用mitmproxy或WireMock,可以模拟更复杂的场景,比如延迟响应、返回错误等。

Mock主要用于开发阶段和异常场景测试,不能完全替代真实接口测试。

# 15. 如何实现失败重试机制?

回答话术:

我用pytest-rerunfailures插件实现失败重试,有两种使用方式:

第一种是全局配置,在pytest.ini中配置重试次数,所有失败用例自动重试。

[pytest]
reruns = 2
reruns_delay = 1

第二种是针对特定用例,用装饰器标注。

@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_unstable_api(self):
    pass

重试适合网络不稳定或偶发性失败的场景,但要注意不能滥用,如果接口本身有问题,重试多少次都会失败,要先定位根本原因。

# 16. 如何实现并发测试?

回答话术:

我使用pytest-xdist插件实现并发执行,通过-n参数指定并发数。

pytest -n 4  # 4个进程并发执行

需要注意三点:

第一是数据隔离,每个用例使用独立的测试数据,避免并发时互相影响。

第二是共享资源加锁,如果多个用例操作同一资源(比如同一个用户),需要加文件锁或分布式锁。

第三是fixture的scope设置,session级别的fixture在并发时每个worker都会执行一次,要根据实际情况调整。

并发能大幅提升执行效率,但也增加了测试的复杂度,要在速度和稳定性之间权衡。

# 17. 如何实现持续集成CI/CD?

回答话术:

我们项目用Jenkins做持续集成,主要配置流程是:

第一步,代码提交触发Jenkins任务,可以是定时触发或Git提交触发。

第二步,拉取最新代码,安装依赖pip install -r requirements.txt

第三步,执行测试用例pytest testcases/ --alluredir=./report

第四步,生成Allure报告allure generate ./report

第五步,发送测试报告,通过邮件或企业微信机器人推送报告链接和测试结果统计。

# 获取测试结果
result = {"total": 100, "pass": 95, "fail": 5}
# 发送通知
send_wechat_notification(result)

关键是要保证测试环境稳定,用例执行快速,失败时能快速定位问题。

# 18. 如何实现接口性能测试?

回答话术:

我主要用Locust做接口性能测试,可以复用接口自动化的API封装。

from locust import HttpUser, task, between

class UserBehavior(HttpUser):
    wait_time = between(1, 3)
    
    @task
    def test_login(self):
        self.client.post("/api/login", 
                        json={"username": "test", "password": "123456"})

执行命令:locust -f locustfile.py --host=http://test.example.com

可以在Web界面设置并发用户数和增长率,实时查看TPS、响应时间、失败率等指标。

性能测试一般在功能测试通过后进行,主要关注接口的响应时间、并发能力、稳定性,发现性能瓶颈。

# 19. 如何管理和维护测试用例?

回答话术:

我从三个方面管理测试用例:

第一是分层分级,用Pytest的mark标记用例优先级(P0、P1、P2)和类型(冒烟、回归、全量),执行时可以灵活选择。

@pytest.mark.smoke
@pytest.mark.P0
def test_login(self):
    pass

第二是定期review,接口变更后及时更新用例,删除过时的用例,补充新功能的用例。

第三是数据驱动,把测试数据和用例逻辑分离,修改数据不影响代码,降低维护成本。

每次迭代后,我会统计用例覆盖率和执行情况,持续优化用例集。

# 20. 框架优化有哪些经验?

回答话术:

我总结了几个优化方向:

性能优化:使用Session复用连接,启用并发执行,缓存不变的数据(如token)。

稳定性优化:增加重试机制,优化等待策略,做好异常处理和兜底逻辑。

可维护性优化:遵循单一职责原则,每个类只做一件事;抽取公共方法,减少重复代码;规范命名和注释。

可扩展性优化:预留扩展点,比如支持插件化开发;使用设计模式,如工厂模式创建不同环境的配置对象。

监控告警:集成CI/CD后,失败自动通知;记录关键指标(成功率、响应时间),形成趋势图。

最重要的是持续迭代,根据团队反馈和实际问题不断改进框架。

# 二、Web自动化(中等)

# 1. 你在项目中使用的Web自动化测试框架是什么?为什么选择它?

回答话术:

我使用的是 Selenium + Pytest + POM模式 的框架。

选择Selenium是因为它支持多浏览器,生态成熟,API简单易用,能满足大部分Web自动化需求。

Pytest作为测试框架,提供了强大的fixture和参数化功能,配合pytest-html可以生成详细的测试报告。

POM(Page Object Model)页面对象模型让页面元素和测试逻辑分离,页面变化时只需要修改PO类,不影响测试用例,维护成本低。

对于复杂场景,我也会结合Playwright,它执行速度更快,支持自动等待,截图和录屏功能更强大。

# 2. 请描述你设计的Web自动化测试框架的整体架构

回答话术:

我的框架采用分层架构,分为五层:

第一层是Page层,每个页面对应一个PO类,封装页面元素定位和操作方法。

第二层是TestCase层,编写测试用例,调用Page层的方法,只关注业务流程。

第三层是Base层,封装浏览器驱动初始化、公共操作方法如等待、截图、滚动等。

第四层是Config层,管理环境配置、浏览器类型、超时时间等参数。

第五层是Data层,管理测试数据,支持YAML、Excel等格式。

project/
├── pages/           # 页面对象层
├── testcases/       # 测试用例层
├── base/           # 基础封装层
├── config/         # 配置管理层
├── data/           # 测试数据层
└── reports/        # 测试报告

这样设计职责清晰,易于维护和扩展。

# 3. 什么是POM模式?如何实现?

回答话术:

POM是Page Object Model页面对象模型,核心思想是把页面元素定位和操作封装到Page类中,测试用例只调用Page类的方法。

优点是页面变化时只需修改PO类,用例不受影响,提高了代码复用性和可维护性。

class LoginPage:
    def __init__(self, driver):
        self.driver = driver
        # 元素定位
        self.username_input = (By.ID, "username")
        self.password_input = (By.ID, "password")
        self.login_btn = (By.ID, "loginBtn")
    
    def input_username(self, username):
        self.driver.find_element(*self.username_input).send_keys(username)
    
    def input_password(self, password):
        self.driver.find_element(*self.password_input).send_keys(password)
    
    def click_login(self):
        self.driver.find_element(*self.login_btn).click()
    
    def login(self, username, password):
        self.input_username(username)
        self.input_password(password)
        self.click_login()

# 测试用例
def test_login(self):
    login_page = LoginPage(self.driver)
    login_page.login("admin", "123456")
    assert "首页" in self.driver.title

这样页面和用例解耦,维护起来非常方便。

# 4. Selenium中有哪些元素定位方式?你常用哪些?

回答话术:

Selenium提供8种定位方式:

id、name、class_name、tag_name、link_text、partial_link_text、xpath、css_selector

我常用的是:

第一优先用id,唯一且性能最好。

第二用css_selector,语法简洁,性能好,比如#id.class[name='value']

第三用xpath,功能强大,可以通过文本、属性、层级关系定位,适合复杂场景。

# CSS定位
driver.find_element(By.CSS_SELECTOR, "#username")
driver.find_element(By.CSS_SELECTOR, ".login-btn")

# XPath定位
driver.find_element(By.XPATH, "//input[@id='username']")
driver.find_element(By.XPATH, "//button[text()='登录']")
driver.find_element(By.XPATH, "//div[@class='header']//a[1]")

尽量避免用tag_name和link_text,定位不准确容易出错。

# 5. 如何处理页面元素定位不到的问题?

回答话术:

元素定位不到主要有三个原因:

第一是元素还没加载出来,我会用显式等待WebDriverWait,等待元素可见或可点击。

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

element = WebDriverWait(driver, 10).until(
    EC.visibility_of_element_located((By.ID, "username"))
)

第二是元素在iframe中,需要先切换到iframe再定位。

driver.switch_to.frame("iframeName")
driver.find_element(By.ID, "element").click()
driver.switch_to.default_content()  # 切回主页面

第三是定位表达式写错了,我会在浏览器F12控制台验证xpath或css表达式是否正确。

在Base类中我封装了统一的元素查找方法,自动处理等待和异常。

# 6. Selenium中的等待机制有哪些?如何选择?

回答话术:

Selenium有三种等待机制:

强制等待sleep:固定等待时间,不推荐使用,浪费时间且不灵活。

隐式等待implicitly_wait:全局设置,查找元素时如果没找到会等待设定时间再抛异常。

driver.implicitly_wait(10)  # 全局生效

显式等待WebDriverWait:针对特定元素设置等待条件,推荐使用。

WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.ID, "submit"))
)

我的选择策略是:

项目初期设置一个较小的隐式等待,比如5秒兜底。

针对关键元素或加载慢的元素用显式等待,设置具体的等待条件,比如元素可见、可点击、文本包含等。

不使用sleep,除非是验证码倒计时这种必须等待的场景。

# 7. 如何封装BasePage基类?

回答话术:

我会创建一个BasePage基类,封装所有页面的公共操作方法:

class BasePage:
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)
    
    def find_element(self, locator):
        """查找单个元素,自动等待"""
        return self.wait.until(EC.visibility_of_element_located(locator))
    
    def click(self, locator):
        """点击元素"""
        self.find_element(locator).click()
    
    def input_text(self, locator, text):
        """输入文本"""
        element = self.find_element(locator)
        element.clear()
        element.send_keys(text)
    
    def get_text(self, locator):
        """获取文本"""
        return self.find_element(locator).text
    
    def take_screenshot(self, filename):
        """截图"""
        self.driver.save_screenshot(filename)
    
    def switch_to_frame(self, locator):
        """切换iframe"""
        iframe = self.find_element(locator)
        self.driver.switch_to.frame(iframe)
    
    def scroll_to_element(self, locator):
        """滚动到元素"""
        element = self.find_element(locator)
        self.driver.execute_script("arguments[0].scrollIntoView();", element)

所有PO类继承BasePage,就可以直接使用这些方法,避免重复代码。

# 8. 如何处理浏览器驱动的管理?

回答话术:

我用webdriver-manager自动管理驱动,不需要手动下载和配置驱动路径。

from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.service import Service

# 自动下载并使用匹配的ChromeDriver
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service)

在conftest.py中用fixture统一管理driver:

@pytest.fixture(scope="function")
def driver():
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
    driver.maximize_window()
    driver.implicitly_wait(10)
    yield driver
    driver.quit()

这样每个测试用例都能获得一个干净的浏览器实例,用完自动关闭。

支持通过配置文件切换浏览器类型,比如Chrome、Firefox、Edge。

# 9. 如何处理弹窗(alert、confirm、prompt)?

回答话术:

Selenium处理JavaScript弹窗需要切换到alert对象:

# 等待alert出现
alert = WebDriverWait(driver, 10).until(EC.alert_is_present())

# 获取弹窗文本
text = alert.text

# 确认弹窗
alert.accept()

# 取消弹窗
alert.dismiss()

# prompt输入文本
alert.send_keys("test")
alert.accept()

我在BasePage中封装了处理弹窗的方法:

def handle_alert(self, action="accept", text=None):
    alert = self.wait.until(EC.alert_is_present())
    if text:
        alert.send_keys(text)
    if action == "accept":
        alert.accept()
    else:
        alert.dismiss()

这样处理弹窗就很简单了。

# 10. 如何处理文件上传和下载?

回答话术:

文件上传有两种方式:

第一种是input标签,直接send_keys传文件路径。

upload_element = driver.find_element(By.ID, "fileUpload")
upload_element.send_keys("/path/to/file.txt")

第二种是非input标签,用AutoIt或pywinauto模拟Windows窗口操作。

文件下载需要配置浏览器选项:

chrome_options = Options()
prefs = {
    "download.default_directory": "/path/to/download",
    "download.prompt_for_download": False,  # 禁用下载提示
}
chrome_options.add_experimental_option("prefs", prefs)

driver = webdriver.Chrome(options=chrome_options)

下载后需要等待文件下载完成,我会检查文件是否存在:

import time
def wait_for_download(file_path, timeout=30):
    for i in range(timeout):
        if os.path.exists(file_path):
            return True
        time.sleep(1)
    return False

# 11. 如何处理动态元素(元素属性动态变化)?

回答话术:

动态元素主要有两种情况:

第一种是元素id或class动态生成,我会用相对稳定的属性定位,比如:

# 用部分匹配
driver.find_element(By.XPATH, "//div[contains(@id, 'dynamic')]")
driver.find_element(By.CSS_SELECTOR, "[id^='prefix']")  # id以prefix开头
driver.find_element(By.CSS_SELECTOR, "[id$='suffix']")  # id以suffix结尾

# 用其他稳定属性
driver.find_element(By.XPATH, "//input[@name='username']")

第二种是元素位置或内容动态变化,我会通过父元素或兄弟元素定位,使用轴定位:

# 通过父元素定位
driver.find_element(By.XPATH, "//div[@class='parent']//button[text()='提交']")

# 通过兄弟元素定位
driver.find_element(By.XPATH, "//label[text()='用户名']/following-sibling::input")

关键是找到页面中相对稳定的元素作为定位锚点。

# 12. 如何处理表格数据的验证?

回答话术:

表格验证我主要用xpath遍历行和列:

def get_table_data(driver, table_locator):
    """获取表格所有数据"""
    table = driver.find_element(*table_locator)
    rows = table.find_elements(By.TAG_NAME, "tr")
    
    data = []
    for row in rows[1:]:  # 跳过表头
        cols = row.find_elements(By.TAG_NAME, "td")
        row_data = [col.text for col in cols]
        data.append(row_data)
    return data

def find_in_table(driver, search_text):
    """在表格中查找指定内容"""
    xpath = f"//table//td[text()='{search_text}']"
    return driver.find_elements(By.XPATH, xpath)

# 获取特定单元格
cell = driver.find_element(By.XPATH, "//table/tbody/tr[2]/td[3]")  # 第2行第3列

对于复杂表格,我会把数据提取封装到PO类的方法中,用例只关注断言逻辑。

# 13. 如何实现数据驱动测试?

回答话术:

我用pytest的parametrize结合外部数据文件实现数据驱动:

# data/login_data.yaml
test_data:
  - username: "admin"
    password: "123456"
    expected: "登录成功"
  - username: "admin"
    password: "wrong"
    expected: "密码错误"

# testcase
import yaml

def load_test_data():
    with open('data/login_data.yaml') as f:
        return yaml.safe_load(f)['test_data']

@pytest.mark.parametrize('data', load_test_data())
def test_login(driver, data):
    login_page = LoginPage(driver)
    login_page.login(data['username'], data['password'])
    assert data['expected'] in login_page.get_message()

这样测试数据和用例分离,非技术人员也能维护数据,扩展性强。

# 14. 如何处理滑动验证码?

回答话术:

滑动验证码处理比较复杂,我主要用ActionChains模拟滑动:

from selenium.webdriver import ActionChains

def slide_verify(driver, slider_element, distance):
    """滑动验证码"""
    action = ActionChains(driver)
    action.click_and_hold(slider_element)  # 按住滑块
    action.move_by_offset(distance, 0)     # 水平移动
    action.release()                        # 释放
    action.perform()                        # 执行

# 或者分段滑动更像人工操作
def slide_verify_human_like(driver, slider, distance):
    action = ActionChains(driver)
    action.click_and_hold(slider)
    
    # 分段移动,模拟人工滑动
    moved = 0
    while moved < distance:
        x = random.randint(10, 20)
        action.move_by_offset(x, random.randint(-2, 2))
        moved += x
        time.sleep(random.uniform(0.01, 0.05))
    
    action.release().perform()

对于复杂的验证码,可以集成打码平台API或者用图像识别算法计算滑动距离。

测试环境建议让开发提供bypass机制,绕过验证码。

# 15. 如何实现失败截图和录屏?

回答话术:

我在conftest.py中用hook函数实现失败自动截图:

@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item):
    outcome = yield
    report = outcome.get_result()
    
    if report.when == "call" and report.failed:
        # 获取driver
        driver = item.funcargs.get('driver')
        if driver:
            # 截图
            screenshot_path = f"screenshots/{item.name}.png"
            driver.save_screenshot(screenshot_path)
            
            # 附加到Allure报告
            with open(screenshot_path, 'rb') as f:
                allure.attach(f.read(), name="失败截图", 
                            attachment_type=allure.attachment_type.PNG)

录屏我用opencv或者浏览器的cdp协议:

# 使用chrome的录屏功能
from selenium.webdriver.common.action_chains import ActionChains

driver.execute_cdp_cmd('Page.startScreencast', {})

# 测试执行...

driver.execute_cdp_cmd('Page.stopScreencast', {})

失败截图对定位问题非常有帮助,特别是在CI环境中。

# 16. 如何实现多浏览器兼容性测试?

回答话术:

我通过配置文件和fixture实现多浏览器支持:

# config.yaml
browser: chrome  # 可选chrome, firefox, edge

# conftest.py
@pytest.fixture(scope="function")
def driver(request):
    browser = request.config.getoption("--browser", default="chrome")
    
    if browser == "chrome":
        driver = webdriver.Chrome()
    elif browser == "firefox":
        driver = webdriver.Firefox()
    elif browser == "edge":
        driver = webdriver.Edge()
    
    driver.maximize_window()
    yield driver
    driver.quit()

# 命令行执行
pytest --browser=firefox

也可以用parametrize实现同一用例在多个浏览器上执行:

@pytest.fixture(params=["chrome", "firefox"])
def multi_browser(request):
    if request.param == "chrome":
        driver = webdriver.Chrome()
    else:
        driver = webdriver.Firefox()
    yield driver
    driver.quit()

结合Selenium Grid可以实现分布式并行测试。

# 17. 如何处理页面跳转和新窗口切换?

回答话术:

Selenium处理多窗口需要切换句柄:

# 记录当前窗口句柄
current_window = driver.current_window_handle

# 点击打开新窗口的链接
driver.find_element(By.LINK_TEXT, "新窗口").click()

# 获取所有窗口句柄
all_windows = driver.window_handles

# 切换到新窗口
for window in all_windows:
    if window != current_window:
        driver.switch_to.window(window)
        break

# 在新窗口操作...

# 关闭新窗口
driver.close()

# 切回原窗口
driver.switch_to.window(current_window)

我在BasePage中封装了窗口切换方法:

def switch_to_new_window(self):
    """切换到最新打开的窗口"""
    self.driver.switch_to.window(self.driver.window_handles[-1])

def switch_to_window_by_title(self, title):
    """根据标题切换窗口"""
    for handle in self.driver.window_handles:
        self.driver.switch_to.window(handle)
        if title in self.driver.title:
            return True
    return False

# 18. 如何提高用例执行速度?

回答话术:

我从四个方面优化执行速度:

第一是并发执行,用pytest-xdist插件多进程运行。

pytest -n 4  # 4个进程并发

第二是减少等待时间,用显式等待替代固定等待,设置合理的超时时间。

第三是复用浏览器session,用例间不重启浏览器,只清理cookie和缓存。

@pytest.fixture(scope="session")
def driver():
    driver = webdriver.Chrome()
    yield driver
    driver.quit()

第四是选择性执行,用标签标记用例优先级,冒烟测试只跑P0用例。

pytest -m "smoke"  # 只运行冒烟用例

通过这些优化,可以将执行时间缩短50%以上。

# 19. Selenium和Playwright有什么区别?如何选择?

回答话术:

Selenium的优势:

  • 生态成熟,文档丰富,社区活跃
  • 支持多语言,团队容易上手
  • 稳定性好,适合长期维护的项目

Playwright的优势:

  • 执行速度更快,自动等待机制更智能
  • 支持多标签页、多浏览器上下文
  • 截图、录屏功能更强大
  • 网络拦截和Mock更方便
  • 支持移动端浏览器模拟
# Playwright示例
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com")
    page.click("#submit")  # 自动等待
    browser.close()

我的选择策略:

  • 新项目优先考虑Playwright
  • 老项目维护继续用Selenium
  • 复杂的网络场景用Playwright
  • 简单的表单测试用Selenium

# 20. Web自动化测试的最佳实践有哪些?

回答话术:

我总结了几点最佳实践:

第一是元素定位稳定性,优先用id,避免用xpath的绝对路径,多用相对定位。

第二是遵循POM模式,页面和用例分离,一个页面一个PO类。

第三是合理使用等待,用显式等待替代sleep,设置明确的等待条件。

第四是做好断言,不只断言元素存在,还要断言文本内容、属性值、页面跳转等。

第五是失败处理机制,失败自动截图、记录日志、支持失败重跑。

第六是用例独立性,每个用例独立运行,不依赖其他用例的执行结果。

第七是持续维护,页面变化及时更新PO类,定期review用例有效性。

第八是合理的用例分层,冒烟、回归、全量分开管理,CI只跑核心用例。

最重要的是根据项目特点灵活调整,没有完美的框架,只有合适的方案。

# 三、App自动化(低)

# 1. 你在项目中使用的App自动化测试框架是什么?为什么选择它?

回答话术:

我使用的是 Appium + Pytest + POM模式 的框架。

选择Appium是因为它跨平台,一套代码可以同时测试Android和iOS,基于WebDriver协议,API和Selenium类似,学习成本低。

Pytest提供了强大的测试管理能力,fixture机制可以很好地管理设备连接、应用启动等前置条件。

POM模式让页面元素和业务逻辑分离,页面改版时只需修改Page类,测试用例不受影响。

对于简单场景,我也会用uiautomator2,它是纯Python实现,执行速度更快,但只支持Android。

# 2. 请描述你设计的App自动化测试框架的整体架构

回答话术:

我的框架分为五层:

第一层是Page层,每个页面一个PO类,封装元素定位和操作方法。

第二层是TestCase层,编写测试场景,调用Page层方法。

第三层是Base层,封装driver初始化、公共操作如滑动、截图、等待等。

第四层是Config层,管理设备配置、desired_capabilities、包名等。

第五层是Utils层,提供日志、断言、数据处理等工具方法。

project/
├── pages/           # 页面对象层
├── testcases/       # 测试用例层
├── base/           # 基础封装层
├── config/         # 配置管理层
├── utils/          # 工具类层
└── reports/        # 测试报告

这样设计职责清晰,支持Android和iOS双平台。

# 3. Appium的核心原理是什么?如何与App交互?

回答话术:

Appium是C/S架构,工作原理是:

第一步,测试脚本发送HTTP请求到Appium Server。

第二步,Appium Server解析请求,转换为对应平台的自动化指令。

第三步,Android平台通过UIAutomator2,iOS平台通过XCUITest框架与App交互。

第四步,操作结果返回给Appium Server,再返回给测试脚本。

# desired_capabilities配置
caps = {
    "platformName": "Android",
    "deviceName": "emulator-5554",
    "appPackage": "com.example.app",
    "appActivity": ".MainActivity",
    "automationName": "UiAutomator2"
}

driver = webdriver.Remote('http://localhost:4723/wd/hub', caps)

Appium不需要修改App源码,通过底层框架实现自动化,这是它的核心优势。

# 4. Android和iOS的元素定位方式有哪些?

回答话术:

Android主要定位方式:

resource-id:唯一标识,优先使用,类似Web的id。

accessibility id:辅助功能id,对应content-desc属性。

xpath:功能强大但性能较差,复杂场景使用。

uiautomator:Android独有,支持UiSelector语法。

# resource-id定位
driver.find_element(AppiumBy.ID, "com.example:id/username")

# accessibility id定位
driver.find_element(AppiumBy.ACCESSIBILITY_ID, "登录按钮")

# xpath定位
driver.find_element(AppiumBy.XPATH, "//android.widget.Button[@text='登录']")

# uiautomator定位
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, 
    'new UiSelector().text("登录")')

iOS主要定位方式:

accessibility id:对应accessibility identifier。

class name:通过控件类型定位。

predicate string:iOS独有,类似SQL查询。

class chain:优化版的xpath。

优先用id,其次accessibility id,避免过度使用xpath。

# 5. 如何获取App的包名和启动Activity?

回答话术:

有三种方式获取:

第一种用aapt命令

# 获取包名和启动Activity
aapt dump badging app.apk | grep package
aapt dump badging app.apk | grep launchable-activity

第二种用adb命令

# 启动App后查看当前Activity
adb shell dumpsys window | findstr mCurrentFocus

# 或者
adb logcat | grep ActivityManager

第三种用Python自动获取

import subprocess

def get_current_activity():
    cmd = "adb shell dumpsys window | grep mCurrentFocus"
    result = subprocess.getoutput(cmd)
    # 解析包名和Activity
    return result

我一般用aapt分析apk包,把配置写入config文件,自动化执行时直接读取。

# 6. 如何设计BasePage类封装公共操作?

回答话术:

我会在BasePage中封装所有页面的通用操作:

class BasePage:
    def __init__(self, driver):
        self.driver = driver
    
    def find_element(self, locator, timeout=10):
        """查找元素,自动等待"""
        return WebDriverWait(self.driver, timeout).until(
            EC.presence_of_element_located(locator)
        )
    
    def click(self, locator):
        """点击元素"""
        self.find_element(locator).click()
    
    def input_text(self, locator, text):
        """输入文本"""
        element = self.find_element(locator)
        element.clear()
        element.send_keys(text)
    
    def get_text(self, locator):
        """获取文本"""
        return self.find_element(locator).text
    
    def swipe_up(self, duration=1000):
        """向上滑动"""
        size = self.driver.get_window_size()
        x = size['width'] * 0.5
        start_y = size['height'] * 0.8
        end_y = size['height'] * 0.2
        self.driver.swipe(x, start_y, x, end_y, duration)
    
    def swipe_to_element(self, locator, max_swipes=10):
        """滑动查找元素"""
        for _ in range(max_swipes):
            if self.is_element_exist(locator):
                return True
            self.swipe_up()
        return False
    
    def take_screenshot(self, filename):
        """截图"""
        self.driver.save_screenshot(filename)

所有Page类继承BasePage,复用这些方法。

# 7. 如何处理Toast提示信息的获取?

回答话术:

Toast是Android的临时提示,需要特殊处理:

方法一:使用xpath定位Toast

def get_toast(driver, timeout=5):
    """获取Toast文本"""
    toast_loc = (AppiumBy.XPATH, 
        "//*[@class='android.widget.Toast']")
    
    try:
        toast = WebDriverWait(driver, timeout).until(
            EC.presence_of_element_located(toast_loc)
        )
        return toast.text
    except:
        return None

# 使用
toast_text = get_toast(driver)
assert "登录成功" in toast_text

方法二:使用uiautomator定位

def get_toast_uiautomator(driver, message):
    """判断Toast是否包含指定文本"""
    toast_element = driver.find_element(
        AppiumBy.ANDROID_UIAUTOMATOR,
        f'new UiSelector().textContains("{message}")'
    )
    return toast_element.text

Toast时间很短,需要及时获取,建议在操作后立即断言。

# 8. 如何实现滑动操作(上下左右滑动)?

回答话术:

滑动操作主要用swipe方法,需要计算坐标:

class SwipeUtil:
    def __init__(self, driver):
        self.driver = driver
        self.size = driver.get_window_size()
        self.width = self.size['width']
        self.height = self.size['height']
    
    def swipe_up(self, duration=1000):
        """向上滑动"""
        x = self.width * 0.5
        start_y = self.height * 0.8
        end_y = self.height * 0.2
        self.driver.swipe(x, start_y, x, end_y, duration)
    
    def swipe_down(self, duration=1000):
        """向下滑动"""
        x = self.width * 0.5
        start_y = self.height * 0.2
        end_y = self.height * 0.8
        self.driver.swipe(x, start_y, x, end_y, duration)
    
    def swipe_left(self, duration=1000):
        """向左滑动"""
        y = self.height * 0.5
        start_x = self.width * 0.8
        end_x = self.width * 0.2
        self.driver.swipe(start_x, y, end_x, y, duration)
    
    def swipe_right(self, duration=1000):
        """向右滑动"""
        y = self.height * 0.5
        start_x = self.width * 0.2
        end_x = self.width * 0.8
        self.driver.swipe(start_x, y, end_x, y, duration)

滑动距离和速度要合理,太快可能滑动失败,太慢影响执行效率。

# 9. 如何处理WebView和Native切换?

回答话术:

混合App中需要在Native和WebView之间切换:

def switch_to_webview(driver, timeout=10):
    """切换到WebView"""
    # 等待WebView加载
    WebDriverWait(driver, timeout).until(
        lambda d: len(d.contexts) > 1
    )
    
    # 获取所有context
    contexts = driver.contexts
    print(f"可用contexts: {contexts}")
    
    # 切换到WebView
    for context in contexts:
        if 'WEBVIEW' in context:
            driver.switch_to.context(context)
            print(f"已切换到: {context}")
            break

def switch_to_native(driver):
    """切换回Native"""
    driver.switch_to.context('NATIVE_APP')
    print("已切换到Native")

# 使用示例
switch_to_webview(driver)
driver.find_element(By.ID, "username").send_keys("test")  # WebView定位
switch_to_native(driver)
driver.find_element(AppiumBy.ID, "com.example:id/btn").click()  # Native定位

WebView中可以用Web元素定位方式,切换context是关键。

# 10. 如何实现App的安装、卸载和启动?

回答话术:

Appium和adb都可以实现:

使用Appium的Desired Capabilities:

# 自动安装并启动
caps = {
    "platformName": "Android",
    "deviceName": "device_id",
    "app": "/path/to/app.apk",  # 自动安装
    "noReset": False,  # 重置App数据
    "fullReset": True,  # 卸载重装
}

# 不安装,直接启动已安装的App
caps = {
    "appPackage": "com.example.app",
    "appActivity": ".MainActivity",
    "noReset": True,  # 不重置,保留数据
}

使用adb命令:

import subprocess

def install_app(apk_path):
    """安装App"""
    cmd = f"adb install -r {apk_path}"  # -r覆盖安装
    subprocess.run(cmd, shell=True)

def uninstall_app(package_name):
    """卸载App"""
    cmd = f"adb uninstall {package_name}"
    subprocess.run(cmd, shell=True)

def start_app(package, activity):
    """启动App"""
    cmd = f"adb shell am start -n {package}/{activity}"
    subprocess.run(cmd, shell=True)

def stop_app(package):
    """关闭App"""
    cmd = f"adb shell am force-stop {package}"
    subprocess.run(cmd, shell=True)

自动化测试建议每次都重置App数据,保证用例独立性。

# 11. 如何处理权限弹窗(定位、相机、通知等)?

回答话术:

权限弹窗有两种处理方式:

方法一:在代码中处理弹窗

def handle_permission_alert(driver):
    """处理权限弹窗"""
    try:
        # Android系统弹窗
        allow_btn = driver.find_element(
            AppiumBy.ID, "com.android.packageinstaller:id/permission_allow_button"
        )
        allow_btn.click()
    except:
        pass

# 启动App后立即处理
driver = webdriver.Remote('http://localhost:4723/wd/hub', caps)
handle_permission_alert(driver)

方法二:通过Capabilities自动授权(推荐)

caps = {
    "platformName": "Android",
    "appPackage": "com.example.app",
    "appActivity": ".MainActivity",
    "autoGrantPermissions": True,  # 自动授予所有权限
}

方法三:通过adb预先授权

adb shell pm grant com.example.app android.permission.CAMERA
adb shell pm grant com.example.app android.permission.ACCESS_FINE_LOCATION

推荐用autoGrantPermissions,简单可靠。

# 12. 如何实现列表滑动查找元素?

回答话术:

列表滑动查找需要循环滑动直到找到目标元素:

def find_element_by_swipe(driver, locator, max_swipes=10):
    """滑动查找元素"""
    for i in range(max_swipes):
        try:
            element = driver.find_element(*locator)
            return element
        except:
            # 未找到,继续滑动
            swipe_up(driver)
    
    raise Exception(f"滑动{max_swipes}次仍未找到元素")

def find_element_by_text(driver, text, max_swipes=10):
    """通过文本滑动查找"""
    for i in range(max_swipes):
        try:
            element = driver.find_element(
                AppiumBy.ANDROID_UIAUTOMATOR,
                f'new UiSelector().text("{text}")'
            )
            return element
        except:
            swipe_up(driver)
    return None

# 使用示例
element = find_element_by_text(driver, "目标商品名称")
element.click()

注意要设置最大滑动次数,避免无限循环。

# 13. 如何实现多设备并发测试?

回答话术:

多设备并发需要启动多个Appium Server,每个设备连接一个:

# conftest.py
import pytest

devices = [
    {"udid": "device1", "port": 4723},
    {"udid": "device2", "port": 4725},
]

@pytest.fixture(params=devices)
def driver(request):
    device = request.param
    caps = {
        "platformName": "Android",
        "udid": device["udid"],
        "appPackage": "com.example.app",
        "appActivity": ".MainActivity",
        "systemPort": 8200 + devices.index(device),  # 避免端口冲突
    }
    
    driver = webdriver.Remote(
        f'http://localhost:{device["port"]}/wd/hub',
        caps
    )
    yield driver
    driver.quit()

# 执行
pytest -n 2  # 两个进程并发

启动多个Appium Server:

appium -p 4723 -bp 4724 -U device1
appium -p 4725 -bp 4726 -U device2

关键是端口号和systemPort不能冲突。

# 14. 如何处理图片验证码?

回答话术:

图片验证码有三种处理方式:

方法一:截图后OCR识别

from PIL import Image
import pytesseract

def recognize_captcha(driver):
    """识别验证码"""
    # 截图
    driver.save_screenshot("screen.png")
    
    # 获取验证码元素位置
    element = driver.find_element(AppiumBy.ID, "captcha_img")
    location = element.location
    size = element.size
    
    # 裁剪验证码图片
    img = Image.open("screen.png")
    left = location['x']
    top = location['y']
    right = left + size['width']
    bottom = top + size['height']
    captcha = img.crop((left, top, right, bottom))
    
    # OCR识别
    code = pytesseract.image_to_string(captcha)
    return code.strip()

方法二:集成打码平台

def get_captcha_from_api(image_base64):
    """调用打码平台API"""
    response = requests.post(
        "http://api.captcha.com/recognize",
        json={"image": image_base64}
    )
    return response.json()["code"]

方法三:测试环境绕过验证码

让开发提供测试账号或万能验证码,这是最推荐的方式。

# 15. 如何实现Android和iOS双平台兼容?

回答话术:

通过配置和条件判断实现双平台兼容:

# config.yaml
android:
  platformName: Android
  deviceName: emulator-5554
  appPackage: com.example.app
  appActivity: .MainActivity

ios:
  platformName: iOS
  deviceName: iPhone 13
  bundleId: com.example.app

# base_page.py
class BasePage:
    def __init__(self, driver):
        self.driver = driver
        self.platform = driver.capabilities['platformName']
    
    def find_element_compatible(self, android_locator, ios_locator):
        """双平台元素定位"""
        if self.platform == 'Android':
            return self.driver.find_element(*android_locator)
        else:
            return self.driver.find_element(*ios_locator)

# page对象
class LoginPage(BasePage):
    def __init__(self, driver):
        super().__init__(driver)
        
        # 定义双平台元素
        if self.platform == 'Android':
            self.username_input = (AppiumBy.ID, "com.example:id/username")
        else:
            self.username_input = (AppiumBy.ACCESSIBILITY_ID, "username")
    
    def input_username(self, username):
        element = self.driver.find_element(*self.username_input)
        element.send_keys(username)

关键是抽象公共操作,差异化部分用条件判断。

# 16. 如何实现失败重跑和失败截图?

回答话术:

使用pytest插件实现失败重跑和截图:

# conftest.py
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item):
    outcome = yield
    report = outcome.get_result()
    
    if report.when == "call" and report.failed:
        driver = item.funcargs.get('driver')
        if driver:
            # 失败截图
            timestamp = time.strftime("%Y%m%d_%H%M%S")
            screenshot_path = f"screenshots/{item.name}_{timestamp}.png"
            driver.save_screenshot(screenshot_path)
            
            # 附加到Allure报告
            with open(screenshot_path, 'rb') as f:
                allure.attach(
                    f.read(),
                    name="失败截图",
                    attachment_type=allure.attachment_type.PNG
                )

# pytest.ini
[pytest]
reruns = 2  # 失败重跑2次
reruns_delay = 1  # 重跑间隔1秒

也可以对单个用例设置重跑:

@pytest.mark.flaky(reruns=3)
def test_unstable_feature(driver):
    pass

# 17. 如何进行弱网络测试?

回答话术:

弱网络测试有两种方式:

方法一:使用Appium的网络模拟

# 设置网络类型
driver.set_network_connection(6)  # 6表示WiFi+数据

# 网络类型常量
# 0: 无网络
# 1: 飞行模式
# 2: 仅WiFi
# 4: 仅数据
# 6: WiFi+数据

# 断网测试
driver.set_network_connection(0)
# 执行操作,验证无网络提示
driver.set_network_connection(6)  # 恢复网络

方法二:使用Charles或Fiddler代理

caps = {
    "platformName": "Android",
    "proxy": {
        "proxyType": "manual",
        "httpProxy": "192.168.1.100:8888",  # Charles代理地址
    }
}

在Charles中设置限速、丢包等弱网络场景。

方法三:使用adb命令限速

# 限制网速
adb shell tc qdisc add dev wlan0 root netem delay 500ms

# 18. 如何获取App性能数据(CPU、内存、流量)?

回答话术:

通过adb命令获取App性能数据:

import subprocess

class PerformanceMonitor:
    def __init__(self, package_name):
        self.package = package_name
    
    def get_cpu_usage(self):
        """获取CPU使用率"""
        cmd = f"adb shell top -n 1 | grep {self.package}"
        result = subprocess.getoutput(cmd)
        # 解析CPU数据
        cpu = result.split()[2]  # 根据实际输出调整
        return cpu
    
    def get_memory_usage(self):
        """获取内存使用"""
        cmd = f"adb shell dumpsys meminfo {self.package}"
        result = subprocess.getoutput(cmd)
        # 解析内存数据
        lines = result.split('\n')
        for line in lines:
            if 'TOTAL' in line:
                memory = line.split()[1]
                return f"{int(memory)/1024:.2f} MB"
    
    def get_network_usage(self):
        """获取网络流量"""
        cmd = f"adb shell cat /proc/net/xt_qtaguid/stats | grep {self.package}"
        result = subprocess.getoutput(cmd)
        # 解析流量数据
        return result

# 使用示例
monitor = PerformanceMonitor("com.example.app")

# 测试前记录基线
baseline_memory = monitor.get_memory_usage()

# 执行测试...

# 测试后对比
current_memory = monitor.get_memory_usage()
assert current_memory < baseline_memory * 1.5  # 内存增长不超过50%

也可以用Appium的性能日志:

caps = {
    "enablePerformanceLogging": True
}
logs = driver.get_log('performance')

# 19. 如何实现App启动时间测试?

回答话术:

测试启动时间主要用adb命令:

import subprocess
import re

def measure_app_launch_time(package, activity):
    """测量App启动时间"""
    # 先关闭App
    subprocess.run(f"adb shell am force-stop {package}", shell=True)
    
    # 启动并获取时间
    cmd = f"adb shell am start -W -n {package}/{activity}"
    result = subprocess.getoutput(cmd)
    
    # 解析启动时间
    # TotalTime: 总启动时间
    total_time = re.findall(r'TotalTime: (\d+)', result)
    if total_time:
        return int(total_time[0])
    return None

# 多次测试取平均值
launch_times = []
for i in range(5):
    time = measure_app_launch_time("com.example.app", ".MainActivity")
    launch_times.append(time)
    print(f"第{i+1}次启动时间: {time}ms")

avg_time = sum(launch_times) / len(launch_times)
print(f"平均启动时间: {avg_time}ms")

# 断言启动时间
assert avg_time < 3000, f"启动时间{avg_time}ms超过3秒"

冷启动和热启动要分别测试。

# 20. App自动化测试的最佳实践有哪些?

回答话术:

我总结了几点最佳实践:

第一是元素定位稳定性,优先用resource-id,避免用xpath的索引定位,多用text或content-desc。

第二是等待机制,用显式等待WebDriverWait,不用固定sleep,每个操作后等待页面稳定。

第三是用例独立性,每个用例独立运行,测试前reset App,清理数据,避免用例间依赖。

第四是异常处理,处理好权限弹窗、网络异常、崩溃等场景,失败自动截图记录现场。

第五是POM模式,页面元素和业务逻辑分离,一个页面一个PO类。

第六是参数化数据驱动,测试数据外部管理,支持批量执行。

第七是CI集成,连接真机或云测平台,定时执行冒烟测试,失败及时通知。

第八是性能监控,关注启动时间、内存、CPU,提前发现性能问题。

第九是日志管理,记录详细的操作日志和设备日志,便于问题定位。

第十是合理分层,冒烟、回归、专项测试分开管理,根据场景选择执行。

最重要的是根据项目特点灵活调整,持续优化框架。


🔗测试开发训练营 (opens new window):针对面试求职的测试开发训练营,大厂导师 1v1 私教,帮助学员从 0 到 1 拿到测开满意的 offer

  • ✅直播授课+实时互动答疑,课程到就业贯穿企业级案例,由测试开发导师全程主讲,绝无其他助理老师代课。
  • ✅大厂测试开发导师一对一求职陪跑,覆盖课程答疑+简历打磨+面试模拟面试+面试复盘等求职辅导

最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。

img

上次更新: 12/2/2025