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 .py
arquivos precisará alterar site-packages/pytest_dependency.py
o 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……