做 UI 自动化有段时间了,在社区也看了大量文章网上也搜集了不少资料资料,自己写代码、调试过程中中摸索了很多东西,踩了不少坑,这篇文章希望能给做 UI 自动化测试小伙伴们在 UI 自动化上有些许帮助。

文本主要介绍下 Pytest+Allure+Appium 记录一些过程和经历,一些好用的方法什么的,之前也没写过什么文章,文章可能有点干,看官们多喝水O(∩_∩) O~

主要用了啥:

Appium 不常见却好用的方法

Appium 直接执行 adb shell 方法
  1. # Appium 启动时增加 --relaxed-security 参数 Appium 即可执行类似adb shell的方法
  2. > appium -p 4723 --relaxed-security
  3. # 使用方法 def adb_shell(self, command, args, includeStderr=False):
  4. """
  5. appium --relaxed-security 方式启动
  6. adb_shell('ps',['|','grep','android'])
  7. :param command:命令
  8. :param args:参数
  9. :param includeStderr: 为 True 则抛异常
  10. :return:
  11. """
  12. result = self.driver.execute_script('mobile: shell', {
  13. 'command': command,
  14. 'args': args,
  15. 'includeStderr': includeStderr,
  16. 'timeout': 5000
  17. })
  18. return result['stdout']

Appium 直接截取元素图片的方法
  1. element = self.driver.find_element_by_id('cn.xxxxxx:id/login_sign')
  2. pngbyte = element.screenshot_as_png
  3. image_data = BytesIO(pngbyte)
  4. img = Image.open(image_data)
  5. img.save('element.png')
  6. # 该方式能直接获取到登录按钮区域的截图

Appium 直接获取手机端日志
  1. # 使用该方法后,手机端 logcat 缓存会清除归零,从新记录
  2. # 建议每条用例执行完执行一边清理,遇到错误再保存减少陈余 log 输出
  3. # Android logcat = self.driver.get_log('logcat')
  4. # iOS 需要安装 brew install libimobiledevice logcat = self.driver.get_log('syslog')
  5. # web 获取控制台日志 logcat = self.driver.get_log('browser')
  6. c = '\n'.join([i['message'] for i in logcat])
  7. allure.attach(c, 'APPlog', allure.attachment_type.TEXT)
  8. #写入到 allure 测试报告中

Appium 直接与设备传输文件
  1. # 发送文件
  2. #Android driver.push_file('/sdcard/element.png', source_path='D:\works\element.png')
  3. # 获取手机文件 png = driver.pull_file('/sdcard/element.png')
  4. with open('element.png', 'wb') as png1:
  5. png1.write(base64.b64decode(png))
  6. # 获取手机文件夹,导出的是zip文件 folder = driver.pull_folder('/sdcard/test')
  7. with open('test.zip', 'wb') as folder1:
  8. folder1.write(base64.b64decode(folder))
  9. # iOS
  10. # 需要安装 ifuse
  11. # > brew install ifuse 或者 > brew cask install osxfuse 或者 自行搜索安装方式
  12. driver.push_file('/Documents/xx/element.png', source_path='D:\works\element.png')
  13. # 向 App 沙盒中发送文件
  14. # iOS 8.3 之后需要应用开启 UIFileSharingEnabled 权限不然会报错 bundleId = 'cn.xxx.xxx' # APP名字 driver.push_file('@{bundleId}:Documents/xx/element.png'.format(bundleId=bundleId), source_path='D:\works\element.png')

Pytest 与 Unittest 初始化上的区别

很多人都使用过 unitest 先说一下 pytest 和 unitest 在 Hook method 上的一些区别

1.Pytest 与 unitest 类似,有些许区别,以下是 Pytest

  1. class TestExample:
  2. def setup(self):
  3. print("setup class:TestStuff")
  4. def teardown(self):
  5. print ("teardown class:TestStuff")
  6. def setup_class(cls):
  7. print ("setup_class class:%s" % cls.__name__)
  8. def teardown_class(cls):
  9. print ("teardown_class class:%s" % cls.__name__)
  10. def setup_method(self, method):
  11. print ("setup_method method:%s" % method.__name__)
  12. def teardown_method(self, method):
  13. print ("teardown_method method:%s" % method.__name__)

2. 使用 pytest.fixture()

  1. @pytest.fixture()
  2. def driver_setup(request):
  3. request.instance.Action = DriverClient().init_driver('android')
  4. def driver_teardown():
  5. request.instance.Action.quit()
  6. request.addfinalizer(driver_teardown)

初始化实例

1.setup_class 方式调用

  1. class Singleton(object):
  2. """单例
  3. ElementActions 为自己封装操作类"""
  4. Action = None
  5. def __new__(cls, *args, **kw):
  6. if not hasattr(cls, '_instance'):
  7. desired_caps={}
  8. host = "http://localhost:4723/wd/hub"
  9. driver = webdriver.Remote(host, desired_caps)
  10. Action = ElementActions(driver, desired_caps)
  11. orig = super(Singleton, cls)
  12. cls._instance = orig.__new__(cls, *args, **kw)
  13. cls._instance.Action = Action
  14. return cls._instance
  15. class DriverClient(Singleton):
  16. pass

测试用例中调用

  1. class TestExample:
  2. def setup_class(cls):
  3. cls.Action = DriverClient().Action
  4. def teardown_class(cls):
  5. cls.Action.clear()
  6. def test_demo(self)
  7. self.Action.driver.launch_app()
  8. self.Action.set_text('123')

2.pytest.fixture() 方式调用

  1. class DriverClient():
  2. def init_driver(self,device_name):
  3. desired_caps={}
  4. host = "http://localhost:4723/wd/hub"
  5. driver = webdriver.Remote(host, desired_caps)
  6. Action = ElementActions(driver, desired_caps)
  7. return Action
  8. # 该函数需要放置在 conftest.py, pytest 运行时会自动拾取 @pytest.fixture()
  9. def driver_setup(request):
  10. request.instance.Action = DriverClient().init_driver()
  11. def driver_teardown():
  12. request.instance.Action.clear()
  13. request.addfinalizer(driver_teardown)

测试用例中调用

  1. #该装饰器会直接引入driver_setup函数 @user3res('driver_setup')
  2. class TestExample:
  3. def test_demo(self):
  4. self.Action.driver.launch_app()
  5. self.Action.set_text('123')

Pytest 参数化方法

1. 第一种方法 parametrize 装饰器参数化方法

  1. @user4ize(('kewords'), [(u"小明"), (u"小红"), (u"小白")])
  2. def test_kewords(self,kewords):
  3. print(kewords)
  4. # 多个参数 @user5ize("test_input,expected", [
  5. ("3+5", 8),
  6. ("2+4", 6),
  7. ("6*9", 42),
  8. ])
  9. def test_eval(test_input, expected):
  10. assert eval(test_input) == expected

2. 第二种方法,使用 pytest hook 批量加参数化

  1. # conftest.py def pytest_generate_tests(metafunc):
  2. """
  3. 使用 hook 给用例加加上参数
  4. metafunc.cls.params 对应类中的 params 参数
  5. """
  6. try:
  7. if metafunc.cls.params and metafunc.function.__name__ in metafunc.cls.params: ## 对应 TestClass params
  8. funcarglist = metafunc.cls.params[metafunc.function.__name__]
  9. argnames = list(funcarglist[0])
  10. metafunc.parametrize(argnames, [[funcargs[name] for name in argnames] for funcargs in funcarglist])
  11. except AttributeError:
  12. pass
  13. # test_demo.py class TestClass:
  14. """
  15. :params 对应 hook 中 metafunc.cls.params
  16. """
  17. # params = Parameterize('TestClass.yaml').getdata()
  18. params = {
  19. 'test_a': [{'a': 1, 'b': 2}, {'a': 1, 'b': 2}],
  20. 'test_b': [{'a': 1, 'b': 2}, {'a': 1, 'b': 2}],
  21. }
  22. def test_a(self, a, b):
  23. assert a == b
  24. def test_b(self, a, b):
  25. assert a == b

Pytest 用例依赖关系

使用 pytest-dependency 库可以创造依赖关系
当上层用例没通过,后续依赖关系用例将直接跳过,可以跨 Class 类筛选
如果需要跨. py 文件运行 需要将 site-packages/pytest_dependency.py 文件的

  1. class DependencyManager(object):
  2. """Dependency manager, stores the results of tests.
  3. """
  4. ScopeCls = {'module':pytest.Module, 'session':pytest.Session}
  5. @classmethod
  6. def getManager(cls, item, scope='session'): # 这里修改成 session

如果

  1. > pip install pytest-dependency
  2. class TestExample(object):
  3. @user7cy()
  4. def test_a(self):
  5. assert False
  6. @user8cy()
  7. def test_b(self):
  8. assert False
  9. @user9cy(depends=["TestExample::test_a"])
  10. def test_c(self):
  11. # TestExample::test_a 没通过则不执行该条用例
  12. # 可以跨 Class 筛选
  13. print("Hello I am in test_c")
  14. @user10cy(depends=["TestExample::test_a","TestExample::test_b"])
  15. def test_d(self):
  16. print("Hello I am in test_d")
  17. pytest -v test_demo.py
  18. 2 failed
  19. - test_1.py:6 TestExample.test_a
  20. - test_1.py:10 TestExample.test_b
  21. 2 skipped

Pytest 自定义标记,执行用例筛选作用

1. 使用 _@_pytest.mark 模块给类或者函数加上标记,用于执行用例时进行筛选

  1. @pytest.mark.webtest
  2. def test_webtest():
  3. pass
  4. @pytest.mark.apitest
  5. class TestExample(object):
  6. def test_a(self):
  7. pass
  8. @pytest.mark.httptest
  9. def test_b(self):
  10. pass

仅执行标记 webtest 的用例

  1. pytest -v -m webtest
  2. Results (0.03s):
  3. 1 passed
  4. 2 deselected

执行标记多条用例

  1. pytest -v -m "webtest or apitest"
  2. Results (0.05s):
  3. 3 passed

仅不执行标记 webtest 的用例

  1. pytest -v -m "not webtest"
  2. Results (0.04s):
  3. 2 passed
  4. 1 deselected

不执行标记多条用例

  1. pytest -v -m "not webtest and not apitest"
  2. Results (0.02s):
  3. 3 deselected

2. 根据 test 节点选择用例

  1. pytest -v Test_example.py::TestClass::test_a
  2. pytest -v Test_example.py::TestClass
  3. pytest -v Test_example.py Test_example2.py

3. 使用 pytest hook 批量标记用例

  1. # conftet.py
  2. def pytest_collection_modifyitems(items):
  3. """
  4. 获取每个函数名字,当用例中含有该字符则打上标记
  5. """
  6. for item in items:
  7. if "http" in item.nodeid:
  8. item.add_marker(pytest.mark.http)
  9. elif "api" in item.nodeid:
  10. item.add_marker(pytest.mark.api)
  11. class TestExample(object):
  12. def test_api_1(self):
  13. pass
  14. def test_api_2(self):
  15. pass
  16. def test_http_1(self):
  17. pass
  18. def test_http_2(self):
  19. pass
  20. def test_demo(self):
  21. pass

仅执行标记 api 的用例

  1. pytest -v -m api
  2. Results (0.03s):
  3. 2 passed
  4. 3 deselected
  5. 可以看到使用批量标记之后,测试用例中只执行了带有 api 的方法

用例错误处理截图,app 日志等

  1. 第一种使用 python 函数装饰器方法
  1. def monitorapp(function):
  2. """
  3. 用例装饰器,截图,日志,是否跳过等
  4. 获取系统log,Android logcat、ios 使用syslog
  5. """
  6. @wraps(function)
  7. def wrapper(self, *args, **kwargs):
  8. try:
  9. allure.dynamic.description('用例开始时间:{}'.format(datetime.datetime.now()))
  10. function(self, *args, **kwargs)
  11. self.Action.driver.get_log('logcat')
  12. except Exception as E:
  13. f = self.Action.driver.get_screenshot_as_png()
  14. allure.attach(f, '失败截图', allure.attachment_type.PNG)
  15. logcat = self.Action.driver.get_log('logcat')
  16. c = '\n'.join([i['message'] for i in logcat])
  17. allure.attach(c, 'APPlog', allure.attachment_type.TEXT)
  18. raise E
  19. finally:
  20. if self.Action.get_app_pid() != self.Action.Apppid:
  21. raise Exception('设备进程 ID 变化,可能发生崩溃')
  22. return wrapper
  1. 第二种使用 pytest hook 方法 (与方法一选一)
  1. @pytest.hookimpl(tryfirst=True, hookwrapper=True)
  2. def pytest_runtest_makereport(item, call):
  3. Action = DriverClient().Action
  4. outcome = yield
  5. rep = outcome.get_result()
  6. if rep.when == "call" and rep.failed:
  7. f = Action.driver.get_screenshot_as_png()
  8. allure.attach(f, '失败截图', allure.attachment_type.PNG)
  9. logcat = Action.driver.get_log('logcat')
  10. c = '\n'.join([i['message'] for i in logcat])
  11. allure.attach(c, 'APPlog', allure.attachment_type.TEXT)
  12. if Action.get_app_pid() != Action.apppid:
  13. raise Exception('设备进程 ID 变化,可能发生崩溃')

Pytest 另一些 hook 的使用方法

1. 自定义 Pytest 参数
  1. > pytest -s -all
  2. # content of conftest.py
  3. def pytest_addoption(parser):
  4. """
  5. 自定义参数
  6. """
  7. parser.addoption("--all", action="store_true",default="type1",help="run all combinations")
  8. def pytest_generate_tests(metafunc):
  9. if 'param' in metafunc.fixturenames:
  10. if metafunc.config.option.all: # 这里能获取到自定义参数
  11. paramlist = [1,2,3]
  12. else:
  13. paramlist = [1,2,4]
  14. metafunc.parametrize("param",paramlist) # 给用例加参数化
  15. # 怎么在测试用例中获取自定义参数呢
  16. # content of conftest.py
  17. def pytest_addoption(parser):
  18. """
  19. 自定义参数
  20. """
  21. parser.addoption("--cmdopt", action="store_true",default="type1",help="run all combinations")
  22. @pytest.fixture
  23. def cmdopt(request):
  24. return request.config.getoption("--cmdopt")
  25. # test_sample.py 测试用例中使用
  26. def test_sample(cmdopt):
  27. if cmdopt == "type1":
  28. print("first")
  29. elif cmdopt == "type2":
  30. print("second")
  31. assert 1
  32. > pytest -q --cmdopt=type2
  33. second
  34. .
  35. 1 passed in 0.09 seconds

2.Pytest 过滤测试目录
  1. #过滤 pytest 需要执行的文件夹或者文件名字
  2. def pytest_ignore_collect(path,config):
  3. if 'logcat' in path.dirname:
  4. return True #返回 True 则该文件不执行

Pytest 一些常用方法

Pytest 用例优先级(比如优先登录什么的)
  1. > pip install pytest-ordering
  2. @pytest.mark.run(order=1)
  3. class TestExample:
  4. def test_a(self):

Pytest 用例失败重试
  1. #原始方法
  2. pytet -s test_demo.py
  3. pytet -s --lf test_demo.py #第二次执行时,只会执行失败的用例
  4. pytet -s --ll test_demo.py #第二次执行时,会执行所有用例,但会优先执行失败用例
  5. #使用第三方插件
  6. pip install pytest-rerunfailures #使用插件
  7. pytest --reruns 2 # 失败case重试两次

Pytest 其他常用参数
  1. pytest --maxfail=10 #失败超过10次则停止运行
  2. pytest -x test_demo.py #出现失败则停止


下一篇文章将计划实战用 Pytest hook 函数运行 yaml 文件来驱动 Appium 做自动化测试,并提供测试源码~
https://testerhome.com/topics/19327