Android UI automation testing using uiautomator2+pytest+allure

Table of contents

Foreword:

introduce

pytest

uiautomator2

allure

Environment build

pytest

uiautomator2

allure

pytest plugin

example

Initialize the driver

fixture mechanism

data sharing

test class

to parameterize

specified order

Run the specified level

Retry

hook function

Affirmation

run

Run the use cases under a certain folder

run a method

run a class

Run P0 level

Run non-P0 level

main mode

Report

failure details

screenshot of failure

Basic operation of uiautomator2

start service

event

click

slide

monitor

view element

Install

start tool

wireless operation


Foreword:

uiautomator2 is a UI automation testing framework based on the Android platform, pytest is a feature-rich Python testing framework, and allure is a tool for generating beautiful test reports. Using these three tools together, you can conduct Android UI automation testing and generate intuitive and visual test reports.

This article mainly explains the use of uiautomator2+pytest+allure for Android UI automation testing. In fact, the main purpose is to write some practical scripts to learn more about the pytest framework.

In addition, by the way, I will introduce the automation framework of uiautomator2, which is also very smooth in use.

I have used appium+testng to write a set of automated scripts before and actually used it in the company. This time, I don’t need to test the company’s app, and use the app of the previous company 58 Tongcheng for automated testing.

introduce

To do UI automation, you must choose a suitable testing framework, such as java’s testng and python’s unittest. The main purpose is to make the code level clear, concise, and reusable. This time, I will introduce the python’s pytest framework.

pytest

pytest 官方:pytest: helps you write better programs — pytest documentation

The pytest framework makes it easy to write small tests, yet scales to support complex functional testing for applications and libraries.

The official introduction is simply to make it easier to write test code without so many constraints. Of course, this piece does not focus on why and how pytest is good, just remember that pytest is a testing framework.

uiautomator2

uiautomator2 is an Android UI automation framework that supports Python to write test scripts to automate devices. The bottom layer is based on Google uiautomator, which belongs to the recently popular openatx project.

The following figure is a schematic diagram of operation:

image

The atx-agent needs to be installed in the device as the server of uiautomator2 to parse the received request and convert it into the code of uiautomator2. Generally speaking, the interaction process is not so cumbersome, and it is indeed much faster than appium in actual use.

allure

allure is a test report, with cool pages and a variety of statistics, a hundred times better than the HTMLTestRunner report, and of course it also supports multiple languages.

Official address: http://allure.qatools.ru

Environment build

Use a mac computer to build an environment

pytest

The latest version is 4.0, but the actual use of 4.0 is somewhat incompatible with allure.
Therefore, it is recommended to use pytest version 3.7

pip install pytest==3.7

uiautomator2

uiautomator2 is also a class library of python, which can be installed with pip.

pip install uiautomator2

allure

brew install allure
pip install pytest-allure-adaptor

With the test framework, automation framework, and test reports, you can basically code.

pytest plugin

The pytest plugin can implement failure retry, print progress, and specify the order

pip install pytest-sugar # 打印进度

pip install pytest-rerunfailures # 失败重试

pip install pytest-ordering # 执行顺序

Of course, there are many more plug-ins, so I won’t introduce them one by one here.

example

Initialize the driver

To do UI automation, you need to initialize a driver object. This driver object can operate click events, slides, double-clicks, etc. The

initialization driver method of uiautomator2

is less than appium configuration, and you can set the global implicit waiting time for elements

import uiautomator2  as ut2
def init_driver(self,device_name):
    '''
    初始化driver
    :return:driver
    '''
    try:
        logger.info(device_name)
        d = ut2.connect(device_name)
        #logger.info("设备信息:{}".format(d.info))
        # 设置全局寻找元素超时时间
        d.wait_timeout = wait_timeout  # default 20.0
        # 设置点击元素延迟时间
        d.click_post_delay = click_post_delay
        #d.service("uiautomator").stop()
        # 停止uiautomator 可能和atx agent冲突
        logger.info("连接设备:{}".format(device_name))
        return d
    except Exception as e:
        logger.info("初始化driver异常!{}".format(e))

fixture mechanism

The unittest framework has setup and teardown methods, which are used to initialize and end test operations. pytest uses the @pytest.fixture method to implement setup and teardown.

The following code is to define a driver_setup method to initialize and end.

# 当设置autouse为True时,
# 在一个session内的所有的test都会自动调用这个fixture
@pytest.fixture()
def driver_setup(request):
    logger.info("自动化测试开始!")
    request.instance.driver = Driver().init_driver(device_name)
    logger.info("driver初始化")
    request.instance.driver.app_start(pck_name, lanuch_activity, stop=True)
    time.sleep(lanuch_time)
    allow(request.instance.driver)
    def driver_teardown():
        logger.info("自动化测试结束!")
        request.instance.driver.app_stop(pck_name)
    request.addfinalizer(driver_teardown)

In addition, there is another way to achieve it, which can be understood as setup and teardown in one method, and pause through the yield keyword.

@pytest.fixture()
def init(self,scope="class"):
    self.home = Home(self.driver)
    self.home.news_tab()
    self.news = News(self.driver)
    logger.info("初始化消息模块")
    yield self.news
    logger.info("结束消息模块")

The yield keyword is used in python syntax generators and iterators to save memory.
For example, for looping a large list, it is a waste of performance to loop it out at one time.
So the yield keyword is used to control the loop.

The following demonstrates yield:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

def yt():
    print "第一次打印"
    yield 0
    print("第二次打印")

if __name__ == '__main__':
    a = yt()
    print next(a)
    print next(a)

If you call the yt function directly, you will find that nothing can be printed out, because at this time, the yt function is only declared and not actually used.

Use the next method to call the first time, and the input result is as follows:

yield is equivalent to return 0 at this time, and the "second print" will not be output at this time, and it will stop at this block.

第一次打印
0

Use the next method to call the second time, and the input results are as follows:

第二次打印

Let's review the above example again:

the setup operation is completed before yield and the self.news object is returned, and

the teardown operation is completed after yield

@pytest.fixture()
def init(self,scope="class"):
    self.home = Home(self.driver)
    self.home.news_tab()
    self.news = News(self.driver)
    logger.info("初始化消息模块")
    yield self.news
    logger.info("结束消息模块")

data sharing

In pytest, you only need to write the conftest.py class to achieve data sharing, and you can automatically find some configurations without importing.

The initialization driver_setup function just mentioned can be set in the conftest.py class. At this time, this function is a global function. It can be used in the test class as follows:
use the @pytest.mark.usefixtures decorator to reference the driver_setup function

@allure.feature("测试发布")
@pytest.mark.usefixtures('driver_setup')
class TestNews:

    @pytest.fixture(params=item)
    def item(self, request):
        return request.param

test class

pytest detects that if it is a class at the beginning of test or at the end of test, it is considered to be a test class that can be executed.

Write the test method at the beginning of test in the test class

@allure.story('测试首页搜索')
def test_home_search(self,init):
    init.home_search()

to parameterize

Suppose the scene is to search for multiple words on the home page, which needs to be completed with parameterization

Use @pytest.mark.parametrize

@pytest.mark.parametrize(('kewords'), [(u"司机"), (u"老师"), (u"公寓")])
def test_home_moresearch(self, init,kewords):
    init.home_more_search(kewords)

specified order

Assuming that you need to log in first to publish a use case. You can log in first and then publish by sorting the use cases

Using @pytest.mark.run, odrer is executed first from small to large

@pytest.mark.usefixtures('driver_setup')
@pytest.mark.run(order=1)
# 指定login先执行
class TestLogin:

Run the specified level

Assume that many use cases have been written, and some use cases are smoke use cases, which can be run at a specified level.
Use @pytest.mark.P0

@allure.story('测试首页更多')
@pytest.mark.P0
def test_home_more(self, init):
    init.home_more()

Command line execution: pytest -v -m "P0", will execute all P0 level use cases

Retry

At this time, you need to use the pytest-rerunfailures plug-in, which is used as follows:

@pytest.mark.flaky(reruns=5, reruns_delay=2)
@allure.story('测试精选活动')
def test_news_good(self,init):
    init.news_good()

Of course, this method is to specify a case to fail and retry

You can also set the user globally as follows:

pytest --reruns 2 --reruns_delay 2

reruns: the number of retries

reruns_delay: the interval between retries

hook function

Define the @pytest.hookimpl function in the conftest.py file, this function can hook the running status of pytest

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    '''
    hook pytest失败
    :param item:
    :param call:
    :return:
    '''
    # execute all other hooks to obtain the report object
    outcome = yield
    rep = outcome.get_result()
    # we only look at actual failing test calls, not setup/teardown
    if rep.when == "call" and rep.failed:
        mode = "a" if os.path.exists("failures") else "w"
        with open("failures", mode) as f:
            # let's also access a fixture for the fun of it
            if "tmpdir" in item.fixturenames:
                extra = " (%s)" % item.funcargs["tmpdir"]
            else:
                extra = ""
            f.write(rep.nodeid + extra + "\n")

It can be seen from the code that relevant information about the failure can be obtained. At that time, with the failure information, you can do things, such as taking screenshots when the use case fails or recording the number of failures for data statistics.

Affirmation

When running a use case, the last step will assert, such as asserting whether the element exists, etc.

def assert_exited(self, element):
    '''
    断言当前页面存在要查找的元素,存在则判断成功
    :param driver:
    :return:
    '''
    if self.find_elements(element):
        logger.info("断言{}元素存在,成功!".format(element))
        assert True
    else:
        logger.info("断言{}元素存在,失败!".format(element))
        assert False

You can also optimize your code like this:

def assert_exited(self, element):
  '''
  断言当前页面存在要查找的元素,存在则判断成功
  :param driver:
  :return:
  '''
  assert self.find_elements(element) == True,"断言{}元素存在,失败!".format(element)
  logger.info("断言{}元素存在,成功!".format(element))

After the assert fails, AssertionError and the defined text will be displayed

AssertionError: 断言xxxxx元素存在,失败!

run

Introduce several common command line operations

Run the use cases under a certain folder

Run all use cases under a file

pytest android/testcase

run a method

class file address::method name

pytest test_home.py::test_home_more

Or use -k parameter + method name

pytest -k test_home_more

run a class

Sometimes it is necessary to debug all the test methods in a test class

directly to keep up with the address of the class file

pytest test_home.py

Run P0 level

pytest -v -m "P0"

Run non-P0 level

pytest -v -m "not P0"

main mode

Write the following code in run.py, which is equivalent to encapsulating the command line parameters into the script.

pytest.main(["-s","--reruns=2", "android/testcase","--alluredir=data"])

Report

After the test code is written, there is still a very good-looking report. In the past, we generally used HTMLTestRunner to report, but the report function of HTMLTestRunner is relatively simple and does not support failure screenshots.

I saw allure's post in the community by chance, and it was a blast to read the presentation report. First, I attach a screenshot of the use case.

image

image

In addition, you can set the reporting level in the code, as follows:

@allure.feature("测试首页")
@pytest.mark.usefixtures('driver_setup')
class TestHome:

    @pytest.fixture()
    def init(self,scope="class"):
        self.home = Home(self.driver)
        logger.info("初始化首页模块")
        yield self.home
        logger.info("结束首页模块")


    @allure.story('测试首页搜索')
    def test_home_search(self,init):
        init.home_search()

Setting allure.feature and allure.story is equivalent to the superior-subordinate relationship.

failure details

Click on a failed use case to see information about the failure

image

screenshot of failure

In the process of running automation, a failure has been encountered, and a screenshot is needed to describe the situation at that time.

In the @pytest.hookimpl function mentioned above, the screenshot method is finally called, and
the screenshot is added using allure.attach.

It should be noted that the second parameter in attach is the binary information of the image.

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    '''
    hook pytest失败
    :param item:
    :param call:
    :return:
    '''
    # execute all other hooks to obtain the report object
    outcome = yield
    rep = outcome.get_result()
    # we only look at actual failing test calls, not setup/teardown
    if rep.when == "call" and rep.failed:
        mode = "a" if os.path.exists("failures") else "w"
        with open("failures", mode) as f:
            # let's also access a fixture for the fun of it
            if "tmpdir" in item.fixturenames:
                extra = " (%s)" % item.funcargs["tmpdir"]
            else:
                extra = ""
            f.write(rep.nodeid + extra + "\n")
        pic_info = adb_screen_shot()
        with allure.step('添加失败截图...'):
            allure.attach("失败截图", pic_info, allure.attach_type.JPG)    

image

Basic operation of uiautomator2

start service

Execute the following command:

python -m uiautomator2 init

will install atx-agent.apk on the phone and start the service on the phone

2018-12-14 18:03:50,691 - __main__.py:327 - INFO - Detect pluged devices: [u'a3f8ca3a']
2018-12-14 18:03:50,693 - __main__.py:343 - INFO - Device(a3f8ca3a) initialing ...
2018-12-14 18:03:51,154 - __main__.py:133 - INFO - install minicap
2018-12-14 18:03:51,314 - __main__.py:140 - INFO - install minitouch
2018-12-14 18:03:51,743 - __main__.py:168 - INFO - apk(1.1.7) already installed, skip
2018-12-14 18:03:51,744 - __main__.py:350 - INFO - atx-agent is already running, force stop
2018-12-14 18:03:52,308 - __main__.py:213 - INFO - atx-agent(0.5.0) already installed, skip
2018-12-14 18:03:52,490 - __main__.py:254 - INFO - launch atx-agent daemon
2018-12-14 18:03:54,568 - __main__.py:273 - INFO - atx-agent version: 0.5.0
atx-agent output: 2018/12/14 18:03:52 [INFO][github.com/openatx/atx-agent] main.go:508: atx-agent listening on 192.168.129.93:7912

The monitor is the ip on the mobile phone + the default is 7921.

event

Event types such as click and slide, etc., introduce several commonly used ones.

click

Locating elements based on id, xpath, and text is not much different from appium.

self.d(resourceId=element).click()
self.d.xpath(element).click()
self.d(text=element).click()

slide

The first 4 parameters are coordinates, and time is the control sliding time

self.d.drag(self.width / 2, self.height * 3 / 4, self.width / 2, self.height / 4, time)

monitor

This is used to start the app for the first time to click the permission or open the screen advertisement

when method is equivalent to the if judgment, and the click will only be clicked if the condition is met, which can generate a lot of logic code.

driver.watcher("允许").when(text="允许").click(text="允许")
driver.watcher("跳过 >").when(text="跳过 >").click(text="跳过 >")
driver.watcher("不要啦").when(text="不要啦").click(text="不要啦")

view element

Install

Need to install weditor library

pip install weditor

start tool

python -m weditor

It will automatically open the browser and display elements, which is equivalent to the web version of uiautomatorviewer, which is more convenient to use.

image

wireless operation

The mobile phone ip mentioned above, if you have this mobile phone ip, you can run the script wirelessly.

Just replace the method in connect with the mobile phone ip.

# d = ut2.connect(device_name)
d = ut2.connect("192.168.129.93")

  As someone who has been here, I also hope that everyone will avoid some detours

Here I will share with you some necessities of the way forward in automated testing, hoping to help you.

(software testing related materials, automated testing related materials, technical questions and answers, etc.)

I believe it can make you better progress!

Click on the small card below

Guess you like

Origin blog.csdn.net/Free355/article/details/131785887