Use Pytest+Appium+Allure para fazer essas coisas de teste de automação de interface do usuário

Eu tenho feito testes de automação de interface do usuário por algum tempo. Eu li muitos artigos na comunidade TesterHome, coletei muitas informações na Internet, adicionei meu próprio código escrito e explorei muitas coisas durante o processo de depuração. Artigo . Espero que possa fornecer alguma ajuda aos amigos que fazem testes de automação de interface do usuário.

O texto apresenta principalmente alguns métodos e experiências úteis para evitar armadilhas no processo de implementação de testes de automação de IU com Pytest+Allure+Appium. O artigo pode estar um pouco seco, os leitores devem beber mais água! O(∩_∩)O~ Se você não entendeu nada, pode entrar no meu grupo de troca de aprendizado no final do artigo para trocar e aprender.

Usado principalmente o que:

  • Python3
  • Ápio
  • Allure-pytest
  • Pytest

Abordagem incomum, mas útil do Appium

1. O Appium executa diretamente o método 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']复制代码

2. O Appium captura diretamente as imagens dos elementos

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. O Appium obtém diretamente os logs do terminal móvel

# 使用该方法后,手机端 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 transfere arquivos diretamente com o dispositivo

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

A diferença entre a inicialização Pytest e Unittest

Muitas pessoas usaram Unitest. Deixe-me falar sobre algumas diferenças entre Pytest e Unitest no método Hook:

1. O Pytest é semelhante ao Unitest, mas com algumas diferenças

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

inicializar instância

1. Chamada do método 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 复制代码

Chamado no caso de teste

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

Chamado no caso de teste

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

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

Métodos parametrizados do Pytest

1. O primeiro método parametriza o método de parametrização do decorador

@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. O segundo método, usando gancho pytest para adicionar parametrização em lotes

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

Dependências de caso de uso do Pytest

As dependências podem ser criadas usando a biblioteca pytest-dependency.

Quando o caso de uso de nível superior falha, os casos de uso de dependência subsequentes são ignorados diretamente e podem ser filtrados pelas classes. Se você precisar executar vários .pyarquivos precisará alterar site-packages/pytest_dependency.pyo arquivo

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

se

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

Tags personalizadas do Pytest para realizar a filtragem de casos de uso

1. Use o módulo @pytest.mark para marcar classes ou funções para triagem ao executar casos de uso

@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 apenas o caso de uso marcado como webtest

pytest -v -m webtest

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

Marca de execução em vários casos de uso

pytest -v -m "webtest or apitest"

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

Apenas o caso de uso que não executa o webtest marcado

pytest -v -m "not webtest"

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

Marque vários casos de uso sem execução

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

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

2. Selecione um caso de uso com base no nó de teste

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

3. Use o gancho pytest para marcar casos de uso em lotes

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

Casos de uso somente de execução para a API do marcador

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

Use capturas de tela de tratamento de erros de caso, logs de aplicativos, etc.

1. O primeiro método para usar o decorador de função 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. O segundo método usando gancho pytest (escolha um do método 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 变化,可能发生崩溃') 复制代码

Como usar outros ganchos no Pytest

1. Personalize os parâmetros do 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. Diretório de teste do filtro Pytest

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

Alguns métodos comuns de pytest

1. Prioridade do caso de uso do Pytest (como login prioritário ou algo assim)

> pip install pytest-ordering


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

2. Nova tentativa de falha do caso de teste 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重试两次 复制代码

3. Outros parâmetros comuns do Pytest

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

Resumir

Acima, os problemas comuns e métodos úteis foram resumidos tanto quanto possível, e espero que seja útil para os alunos do teste! O próximo artigo planejará explicar como usar a função de gancho Pytest para executar arquivos yaml para conduzir o Appium para fazer testes automatizados e fornecer código-fonte de teste, portanto, fique atento!


força escassa

Por fim, gostaria de agradecer a todos que leram meu artigo com atenção. Vendo o crescimento e a atenção dos fãs em todo o caminho, há sempre a necessidade de reciprocidade. Embora não seja uma coisa muito valiosa, você pode tirá-la se você preciso disso:

Esses materiais devem ser o depósito de preparação mais abrangente e completo para amigos que desejam avançar [testes automatizados]. Esse depósito também me acompanhou na jornada mais difícil e espero que possa ajudar você também! Tudo deve ser feito o mais cedo possível, especialmente na indústria técnica, devemos melhorar nossas habilidades técnicas. espero ser útil……

Acho que você gosta

Origin blog.csdn.net/m0_58026506/article/details/130174318
Recomendado
Clasificación