小小千想和您聊一聊

当前位置: 首页> 技术分享> Pytest+Appium+Allure 做 UI 自动化的那些事

Pytest+Appium+Allure 做 UI 自动化的那些事

  文本主要介绍下 Pytest+Allure+Appium 记录一些过程和经历。

  法主要用了啥:

  · Python3

  · Appium

  · Allure-pytest

  · Pytest

  Appium 不常见却好用的方法

  Appium 直接执行 adb shell 方法

  # Appium 启动时增加 --relaxed-security 参数 Appium 即可执行类似adb shell的方法

  > appium -p 4723 --relaxed-security

  # 使用方法

  def adb_shell(self, command, args, includeStderr=False):

  """

  appium --relaxed-security 方式启动

  adb_shell('ps',['|','grep','android'])

  :param command:命令

  :param args:参数

  :param includeStderr: 为 True 则抛异常

  :return:

  """

  result = self.driver.execute_script('mobile: shell', {

  'command': command,

  'args': args,

  'includeStderr': includeStderr,

  'timeout': 5000

  })

  return result['stdout']

  Appium 直接截取元素图片的方法

  element = self.driver.find_element_by_id('cn.xxxxxx:id/login_sign')

  pngbyte = element.screenshot_as_png

  image_data = BytesIO(pngbyte)

  img = Image.open(image_data)

  img.save('element.png')

  # 该方式能直接获取到登录按钮区域的截图

  Appium 直接获取手机端日志

  # 使用该方法后,手机端 logcat 缓存会清除归零,从新记录

  # 建议每条用例执行完执行一边清理,遇到错误再保存减少陈余 log 输出

  # Android

  logcat = self.driver.get_log('logcat')

  # iOS 需要安装 brew install libimobiledevice

  logcat = self.driver.get_log('syslog')

  # web 获取控制台日志

  logcat = self.driver.get_log('browser')

  c = '\n'.join([i['message'] for i in logcat])

  allure.attach(c, 'APPlog', allure.attachment_type.TEXT)

  #写入到 allure 测试报告中

  Appium 直接与设备传输文件

  # 发送文件

  #Android

  driver.push_file('/sdcard/element.png', source_path='D:\works\element.png')

  # 获取手机文件

  png = driver.pull_file('/sdcard/element.png')

  with open('element.png', 'wb') as png1:

  png1.write(base64.b64decode(png))

  # 获取手机文件夹,导出的是zip文件

  folder = driver.pull_folder('/sdcard/test')

  with open('test.zip', 'wb') as folder1:

  folder1.write(base64.b64decode(folder))

  # iOS

  # 需要安装 ifuse

  # > brew install ifuse 或者 > brew cask install osxfuse 或者 自行搜索安装方式

  driver.push_file('/Documents/xx/element.png', source_path='D:\works\element.png')

  # 向 App 沙盒中发送文件

  # 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

  class TestExample:

  def setup(self):

  print("setup class:TestStuff")

  def teardown(self):

  print ("teardown class:TestStuff")

  def setup_class(cls):

  print ("setup_class class:%s" % cls.__name__)

  def teardown_class(cls):

  print ("teardown_class class:%s" % cls.__name__)

  def setup_method(self, method):

  print ("setup_method method:%s" % method.__name__)

  def teardown_method(self, method):

  print ("teardown_method method:%s" % method.__name__)

  2.使用 pytest.fixture()

  @pytest.fixture()

  def driver_setup(request):

  request.instance.Action = DriverClient().init_driver('android')

  def driver_teardown():

  request.instance.Action.quit()

  request.addfinalizer(driver_teardown)

  初始化实例

  1.setup_class 方式调用

  class Singleton(object):

  """单例

  ElementActions 为自己封装操作类"""

  Action = None

  def __new__(cls, *args, **kw):

  if not hasattr(cls, '_instance'):

  desired_caps={}

  host = "http://localhost:4723/wd/hub"

  driver = webdriver.Remote(host, desired_caps)

  Action = ElementActions(driver, desired_caps)

  orig = super(Singleton, cls)

  cls._instance = orig.__new__(cls, *args, **kw)

  cls._instance.Action = Action

  return cls._instance

  class DriverClient(Singleton):

  pass

  测试用例中调用

  class TestExample:

  def setup_class(cls):

  cls.Action = DriverClient().Action

  def teardown_class(cls):

  cls.Action.clear()

  def test_demo(self)

  self.Action.driver.launch_app()

  self.Action.set_text('123')

  2.pytest.fixture() 方式调用

  class DriverClient():

  def init_driver(self,device_name):

  desired_caps={}

  host = "http://localhost:4723/wd/hub"

  driver = webdriver.Remote(host, desired_caps)

  Action = ElementActions(driver, desired_caps)

  return Action

  # 该函数需要放置在 conftest.py, pytest 运行时会自动拾取

  @pytest.fixture()

  def driver_setup(request):

  request.instance.Action = DriverClient().init_driver()

  def driver_teardown():

  request.instance.Action.clear()

  request.addfinalizer(driver_teardown)

  测试用例中调用

  #该装饰器会直接引入driver_setup函数

  @pytest.mark.usefixtures('driver_setup')

  class TestExample:

  def test_demo(self):

  self.Action.driver.launch_app()

  self.Action.set_text('123')

  Pytest 参数化方法

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

  @pytest.mark.parametrize(('kewords'), [(u"小明"), (u"小红"), (u"小白")])

  def test_kewords(self,kewords):

  print(kewords)

  # 多个参数

  @pytest.mark.parametrize("test_input,expected", [

  ("3+5", 8),

  ("2+4", 6),

  ("6*9", 42),

  ])

  def test_eval(test_input, expected):

  assert eval(test_input) == expected

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

  # conftest.py

  def pytest_generate_tests(metafunc):

  """

  使用 hook 给用例加加上参数

  metafunc.cls.params 对应类中的 params 参数

  """

  try:

  if metafunc.cls.params and metafunc.function.__name__ in metafunc.cls.params: ## 对应 TestClass params

  funcarglist = metafunc.cls.params[metafunc.function.__name__]

  argnames = list(funcarglist[0])

  metafunc.parametrize(argnames, [[funcargs[name] for name in argnames] for funcargs in funcarglist])

  except AttributeError:

  pass

  # test_demo.py

  class TestClass:

  """

  :params 对应 hook 中 metafunc.cls.params

  """

  # params = Parameterize('TestClass.yaml').getdata()

  params = {

  'test_a': [{'a': 1, 'b': 2}, {'a': 1, 'b': 2}],

  'test_b': [{'a': 1, 'b': 2}, {'a': 1, 'b': 2}],

  }

  def test_a(self, a, b):

  assert a == b

  def test_b(self, a, b):

  assert a == b

  Pytest 用例依赖关系

  使用 pytest-dependency 库可以创造依赖关系

  当上层用例没通过,后续依赖关系用例将直接跳过,可以跨 Class 类筛选

  如果需要跨.py 文件运行 需要将 site-packages/pytest_dependency.py 文件的

  class DependencyManager(object):

  """Dependency manager, stores the results of tests.

  """

  ScopeCls = {'module':pytest.Module, 'session':pytest.Session}

  @classmethod

  def getManager(cls, item, scope='session'): # 这里修改成 session

  如果

  > pip install pytest-dependency

  class TestExample(object):

  @pytest.mark.dependency()

  def test_a(self):

  assert False

  @pytest.mark.dependency()

  def test_b(self):

  assert False

  @pytest.mark.dependency(depends=["TestExample::test_a"])

  def test_c(self):

  # TestExample::test_a 没通过则不执行该条用例

  # 可以跨 Class 筛选

  print("Hello I am in test_c")

  @pytest.mark.dependency(depends=["TestExample::test_a","TestExample::test_b"])

  def test_d(self):

  print("Hello I am in test_d")

  pytest -v test_demo.py

  2 failed

  - test_1.py:6 TestExample.test_a

  - test_1.py:10 TestExample.test_b

  2 skipped

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

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

  @pytest.mark.webtest

  def test_webtest():

  pass

  @pytest.mark.apitest

  class TestExample(object):

  def test_a(self):

  pass

  @pytest.mark.httptest

  def test_b(self):

  pass

  仅执行标记 webtest 的用例

  pytest -v -m webtest

  Results (0.03s):

  1 passed

  2 deselected

  执行标记多条用例

  pytest -v -m "webtest or apitest"

  Results (0.05s):

  3 passed

  仅不执行标记 webtest 的用例

  pytest -v -m "not webtest"

  Results (0.04s):

  2 passed

  1 deselected

  不执行标记多条用例

  pytest -v -m "not webtest and not apitest"

  Results (0.02s):

  3 deselected

  2.根据 test 节点选择用例

  pytest -v Test_example.py::TestClass::test_a

  pytest -v Test_example.py::TestClass

  pytest -v Test_example.py Test_example2.py

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

  # conftet.py

  def pytest_collection_modifyitems(items):

  """

  获取每个函数名字,当用例中含有该字符则打上标记

  """

  for item in items:

  if "http" in item.nodeid:

  item.add_marker(pytest.mark.http)

  elif "api" in item.nodeid:

  item.add_marker(pytest.mark.api)

  class TestExample(object):

  def test_api_1(self):

  pass

  def test_api_2(self):

  pass

  def test_http_1(self):

  pass

  def test_http_2(self):

  pass

  def test_demo(self):

  pass

  仅执行标记 api 的用例

  pytest -v -m api

  Results (0.03s):

  2 passed

  3 deselected

  可以看到使用批量标记之后,测试用例中只执行了带有 api 的方法

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

  1.第一种使用 python 函数装饰器方法

  def monitorapp(function):

  """

  用例装饰器,截图,日志,是否跳过等

  获取系统log,Android logcat、ios 使用syslog

  """

  @wraps(function)

  def wrapper(self, *args, **kwargs):

  try:

  allure.dynamic.description('用例开始时间:{}'.format(datetime.datetime.now()))

  function(self, *args, **kwargs)

  self.Action.driver.get_log('logcat')

  except Exception as E:

  f = self.Action.driver.get_screenshot_as_png()

  allure.attach(f, '失败截图', allure.attachment_type.PNG)

  logcat = self.Action.driver.get_log('logcat')

  c = '\n'.join([i['message'] for i in logcat])

  allure.attach(c, 'APPlog', allure.attachment_type.TEXT)

  raise E

  finally:

  if self.Action.get_app_pid() != self.Action.Apppid:

  raise Exception('设备进程 ID 变化,可能发生崩溃')

  return wrapper

  2.第二种使用 pytest hook 方法 (与方法一选一)

  @pytest.hookimpl(tryfirst=True, hookwrapper=True)

  def pytest_runtest_makereport(item, call):

  Action = DriverClient().Action

  outcome = yield

  rep = outcome.get_result()

  if rep.when == "call" and rep.failed:

  f = Action.driver.get_screenshot_as_png()

  allure.attach(f, '失败截图', allure.attachment_type.PNG)

  logcat = Action.driver.get_log('logcat')

  c = '\n'.join([i['message'] for i in logcat])

  allure.attach(c, 'APPlog', allure.attachment_type.TEXT)

  if Action.get_app_pid() != Action.apppid:

  raise Exception('设备进程 ID 变化,可能发生崩溃')

  Pytest 另一些 hook 的使用方法

  1.自定义 Pytest 参数

  > pytest -s -all

  # content of conftest.py

  def pytest_addoption(parser):

  """

  自定义参数

  """

  parser.addoption("--all", action="store_true",default="type1",help="run all combinations")

  def pytest_generate_tests(metafunc):

  if 'param' in metafunc.fixturenames:

  if metafunc.config.option.all: # 这里能获取到自定义参数

  paramlist = [1,2,3]

  else:

  paramlist = [1,2,4]

  metafunc.parametrize("param",paramlist) # 给用例加参数化

  # 怎么在测试用例中获取自定义参数呢

  # content of conftest.py

  def pytest_addoption(parser):

  """

  自定义参数

  """

  parser.addoption("--cmdopt", action="store_true",default="type1",help="run all combinations")

  @pytest.fixture

  def cmdopt(request):

  return request.config.getoption("--cmdopt")

  # test_sample.py 测试用例中使用

  def test_sample(cmdopt):

  if cmdopt == "type1":

  print("first")

  elif cmdopt == "type2":

  print("second")

  assert 1

  > pytest -q --cmdopt=type2

  second

  .

  1 passed in 0.09 seconds

  2.Pytest 过滤测试目录

  #过滤 pytest 需要执行的文件夹或者文件名字

  def pytest_ignore_collect(path,config):

  if 'logcat' in path.dirname:

  return True #返回 True 则该文件不执行

  Pytest 一些常用方法

  Pytest 用例优先级(比如优先登录什么的)

  > pip install pytest-ordering

  @pytest.mark.run(order=1)

  class TestExample:

  def test_a(self):

  Pytest 用例失败重试

  #原始方法

  pytet -s test_demo.py

  pytet -s --lf test_demo.py #第二次执行时,只会执行失败的用例

  pytet -s --ll test_demo.py #第二次执行时,会执行所有用例,但会优先执行失败用例

  #使用第三方插件

  pip install pytest-rerunfailures #使用插件

  pytest --reruns 2 # 失败case重试两次

  Pytest 其他常用参数

  pytest --maxfail=10 #失败超过10次则停止运行

  pytest -x test_demo.py #出现失败则停止

上一篇:HTML5工具初识之网页编辑器

下一篇:web基础之css选择器

QQ技术交流群

千锋软件测试官方①群
858327674

加入群聊