Use Pytest+Appium+Allure to do those things of UI automation testing

I have been doing UI automation testing for a while. I have read a lot of articles in the TesterHome community, collected a lot of information on the Internet, added my own code writing, and explored a lot of things during the debugging process. Article. I hope it can provide some help to the friends who do UI automation testing.

The text mainly introduces some useful methods and experience of avoiding pitfalls in the process of implementing UI automation testing with Pytest+Allure+Appium. The article may be a bit dry, readers should drink more water! O(∩_∩)O~ If you don’t understand anything, you can join my learning exchange group at the end of the article to exchange and learn.

Mainly used what:

  • Python3
  • Appius
  • Allure-pytest
  • Pytest

Appium's uncommon but useful approach

1. Appium directly executes the adb shell method

# 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']复制代码

2. Appium directly captures element pictures

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')
# 该方式能直接获取到登录按钮区域的截图 复制代码

3. Appium directly obtains the mobile terminal log

# 使用该方法后,手机端 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 测试报告中 复制代码

4. Appium transfers files directly with the device

# 发送文件
#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') 复制代码

The difference between Pytest and Unittest initialization

Many people have used Unitest. Let me talk about some differences between Pytest and Unitest in the Hook method:

1. Pytest is similar to Unitest, but with some differences

The following is 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. Use 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) 复制代码

initialize instance

1. Setup_class method call

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 复制代码

Called in the test case

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() call

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) 复制代码

Called in the test case

#该装饰器会直接引入driver_setup函数
@pytest.mark.usefixtures('driver_setup')
class TestExample:

 def test_demo(self):
        self.Action.driver.launch_app()
        self.Action.set_text('123') 复制代码

Pytest parameterized methods

1. The first method parametrize decorator parameterization method

@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. The second method, using pytest hook to add parameterization in batches

#  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 use case dependencies

Dependencies can be created using the pytest-dependency library.

When the upper-level use case fails, subsequent dependency use cases will be skipped directly, and can be filtered across Classes. If you need to run across .pyfiles you need to change site-packages/pytest_dependency.pythe file's

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 复制代码

if

> 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 custom tags to perform use case filtering

1. Use the @pytest.mark module to mark classes or functions for screening when executing use cases

@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 复制代码

Execute only the use case marked webtest

pytest -v -m webtest

Results (0.03s):
 1 passed
 2 deselected 复制代码

Execution mark multiple use cases

pytest -v -m "webtest or apitest"

Results (0.05s):
 3 passed 复制代码

Only the use case that does not execute the marked webtest

pytest -v -m "not webtest"

Results (0.04s):
 2 passed
 1 deselected 复制代码

Mark multiple use cases without execution

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

Results (0.02s):
 3 deselected 复制代码

2. Select a use case based on the test node

pytest -v Test_example.py::TestClass::test_a
pytest -v Test_example.py::TestClass
pytest -v Test_example.py Test_example2.py 复制代码

3. Use pytest hook to mark use cases in batches

# 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复制代码

Execute only the use case of the marker API

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

Use case error handling screenshots, App logs, etc.

1. The first method to use python function decorator

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. The second method using pytest hook (choose one from method 1)

@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 变化,可能发生崩溃') 复制代码

How to use other hooks in Pytest

1. Customize Pytest parameters

> 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 filter test directory

#过滤 pytest 需要执行的文件夹或者文件名字
def pytest_ignore_collect(path,config):
 if 'logcat' in path.dirname:
 return True #返回 True 则该文件不执行 复制代码

Some common methods of pytest

1. Pytest use case priority (such as priority login or something)

> pip install pytest-ordering


@pytest.mark.run(order=1)
class TestExample:
 def test_a(self):复制代码

2. Pytest test case failure retry

#原始方法
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重试两次 复制代码

3. Other common parameters of Pytest

pytest --maxfail=10 #失败超过10次则停止运行
pytest -x test_demo.py #出现失败则停止 复制代码

Summarize

Above, the common problems and useful methods have been summarized as much as possible, and I hope it will be helpful to the test students! The next article will plan to explain how to use the Pytest hook function to run yaml files to drive Appium to do automated testing, and provide test source code, so stay tuned!


meager strength

Finally, I would like to thank everyone who has read my article carefully. Seeing the fans’ growth and attention all the way, there is always a need for reciprocity. Although it is not a very valuable thing, you can take it away if you need it:

These materials should be the most comprehensive and complete preparation warehouse for friends who want to advance [automated testing]. This warehouse has also accompanied me through the most difficult journey, and I hope it can help you too! Everything should be done as early as possible, especially in the technical industry, we must improve our technical skills. I hope to be helpful……

Guess you like

Origin blog.csdn.net/m0_58026506/article/details/130174318