Android App自动化测试框架【一】

本文为博主原创,未经许可严禁转载。
本文链接:https://blog.csdn.net/zyooooxie/article/details/113841107

2018年我开始在csdn写博客,最初就是在写UI自动化测试;
现如今2021年了,想着为了锻炼自己,写个app的自动化测试框架。

这是 App自动化测试的 category ,有兴趣 可以看看。

个人博客:https://blog.csdn.net/zyooooxie

需求

框架设计的目的:应用在冒烟测试、回归测试阶段;
脚本的用例:核心且稳定的业务;

要实现:

  1. 分层设计【po】;
  2. 用例执行失败时 重跑 + 断言失败、元素查找失败 自动截图 + 自定义用例执行的顺序;
  3. 截图、日志、测试报告的清理;
  4. 邮件提醒【Jenkins构建邮件 + 测试报告邮件】;
  5. 企微机器人提醒;

框架设计

使用: appium + pytest + allure + pytest-ordering + pytest-rerunfailures

  1. 清理本地文件(截图、日志);
  2. 卸载、再安装App;
  3. po 执行用例、 断言;Appium启动session,发送命令;
  4. 清理、重新生成测试报告;
  5. 发送(测试报告)邮件;企微通知;
  6. Jenkins定时构建;邮件通知Jobs构建情况;

具体实现

本系列分享 拿某Q极速版【安卓客户端】举例;

appPackage=csdn.zyooooxie.qqlite
appActivity=csdn.zyooooxie.mobileqq.activity.SplashActivity

本期主要讲述:

在这里插入图片描述

清理本地的 截图、日志文件

File:run_app.py


def delete_file():
    for i in [log_path, screen_ele_path, screen_assert_path]:
        os.chdir(i)
        all_file = os.listdir(i)

        if i == log_path:
            all_file.sort(key=os.path.getmtime, reverse=True)
            # 最新生成的日志 要保留
            all_file = all_file[1:]

        for f in all_file:
            os.remove(f)

卸载、安装App

File:apk_install.py


class ApkInstall(object):
    """adb 命令来执行卸载、安装"""
    apk_path = app_path

    def __init__(self, package='csdn.zyooooxie.qqlite'):
        self.package = package

    def get_apk_name(self):
        os.chdir(self.apk_path)
        all_files = os.listdir(self.apk_path)

        apk = [a for a in all_files if a.endswith('apk')]

        apk_list = sorted(apk, key=lambda b: os.path.getctime(b), reverse=False)

        Log.info(apk_list[-1])
        return apk_list[-1]

    def uninstall_apk(self, first=None):
        # 不加-k  ('-k' means keep the data and cache directories)
        cmd = """adb uninstall {}""".format(self.package)
        r = os.popen(cmd)
        text = r.read()
        r.close()
        
        Log.info(cmd)

        if text.find('Failure') != -1:
            if first is not None:
                raise Exception('卸载报错')
            else:
                Log.info('未安装过,so 卸载失败')

    def install_apk(self, first_install=None, uninstall=None):
        apk = self.get_apk_name()

        if uninstall is not None:
            self.uninstall_apk(first=first_install)
            sleep(5)

        if first_install is None:
            cmd = """adb install -r {}""".format(apk)        # -r  (reinstall) 重装

        else:
            cmd = """adb install {}""".format(apk)

        Log.info(cmd)

        new_text = subprocess.check_output(cmd, shell=True)
        new_text = new_text.decode()
        print(new_text)
        sleep(5)

        if new_text.find('Success') != -1:
            Log.info('安装成功')
        else:
            raise Exception('安装有异常')

File:run_app.py


def install():
    ApkInstall('csdn.zyooooxie.qqlite').install_apk(first_install='yes', uninstall='yes')

appium启动session、退出

File:base_driver.py


class BaseDriver(object):

    def appium_desired(self, devices):
        desired_data = CommonFun.read_config(devices)

        desired_data = {
    
    d[0]: d[1] for d in desired_data}

        desired_caps = deepcopy(desired_data)

        desired_caps.update({
    
    'noReset': eval(desired_data['noReset'])})
        desired_caps.update({
    
    'unicodeKeyboard': eval(desired_data['unicodeKeyboard'])})
        desired_caps.update({
    
    'resetKeyboard': eval(desired_data['resetKeyboard'])})

        desired_caps.pop('ip')
        desired_caps.pop('port')

        u = "http://{}:{}/wd/hub".format(desired_data['ip'], desired_data['port'])

        driver = webdriver.Remote(u, desired_caps)
        
        return driver

    def driver_quit(self, driver):

        # Sending command to android: {"cmd":"shutdown"} + driver.deleteSession() + kill all uiautomator processes
        driver.quit()

po

File:base_method.py

class BaseMethod(object):
    """基础方法"""

    def driver_find_element_and_wait(self, driver: WebDriver, by, location, the_time=10):
        """
        等待元素可见
        :param driver:
        :param by:
        :param location:
        :param the_time:
        :return:
        """

        if by not in MobileBy.__dict__.values() and by not in By.__dict__.values():
            raise NameError("Please enter the correct targeting elements.")

        try:
            ele = WebDriverWait(driver, the_time, poll_frequency=1).until(ec.visibility_of_element_located((by, location)))

        except Exception:
            Log.error('元素查找失败:{} '.format((by, location)))
            img_name = ''.join([time.strftime("%Y%m%d_%H%M%S"), '.png'])
            Log.info('已截图:{}'.format(img_name))
            driver.save_screenshot(os.path.join(screen_ele_path, img_name))
            raise NoSuchElementException

        return ele

    def element_click(self, driver: WebDriver, by, location, the_time=10):
        self.driver_find_element_and_wait(driver, by, location, the_time).click()

    def element_send_keys(self, driver: WebDriver, keys, by, location, the_time=10):
        self.driver_find_element_and_wait(driver, by, location, the_time).clear().send_keys(keys)

    def assert_FindElement(self, driver: WebDriver, by, location, the_time=10):
        try:
            WebDriverWait(driver, the_time).until(ec.visibility_of_element_located((by, location)), '查找失败')
        # TimeoutException
        except Exception as e:
            Log.debug(repr(e))
            Log.error('断言时 元素找不到:{}'.format((by, location)))

            img_name = ''.join([time.strftime("%Y%m%d_%H%M%S"), '.png'])
            Log.info('已截图:{}'.format(img_name))
            driver.save_screenshot(os.path.join(screen_assert_path, img_name))
            raise AssertionError

    def assert_app(self, driver: WebDriver, app_name):
        print(driver.current_activity, driver.current_package)
        assert driver.current_activity != app_name

File:page_test.py

class PageTest(BaseMethod):

    # 暂不使用
    not_in_use = (By.ID, 'csdn.zyooooxie.qqlite:id/dialogLeftBtn')

    # 同意
    in_use = (By.ID, 'csdn.zyooooxie.qqlite:id/dialogRightBtn')
    in_use2 = (MobileBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("同意")')
    in_use3 = (MobileBy.ANDROID_UIAUTOMATOR, 'new UiSelector().description("同意")')
    in_use4 = (MobileBy.ACCESSIBILITY_ID, '同意')
    in_use5 = (MobileBy.ID, 'dialogRightBtn')

    # 服务协议
    service_agreement = (By.ID, 'csdn.zyooooxie.qqlite:id/dialogText')

    login = (By.ID, 'csdn.zyooooxie.qqlite:id/btn_login')

    # 完全不存在
    fail = (By.ID, 'csdn.zyooooxie.qqlite:id/fail')

    # 选择 暂不使用
    def choose_NotInUse(self, driver: WebDriver):
        self.driver_find_element_and_wait(driver, *self.not_in_use).click()
        sleep(2)

    # 点击 服务协议
    def click_Service(self, driver: WebDriver):
        self.driver_find_element_and_wait(driver, *self.service_agreement).click()
        sleep(2)

File:test_test.py


@pytest.mark.first
@allure.feature('测试TEST')
class TestTest(PageTest):

    @allure.story('暂不使用')
    @allure.title('用例1')
    @mark_smoke
    def test_0(self, driver):
        allure.dynamic.description("动态description0")

        self.choose_NotInUse(driver)
        time.sleep(1)
        self.assert_app(driver, CommonFun.read_config('phone_honor', option='appPackage'))

    @allure.story('服务协议')
    @allure.title('用例2')
    def test_1(self, driver):
        allure.dynamic.description("动态description1")

        self.click_Service(driver)
        self.assert_FindElement(driver, *self.not_in_use)

    # @pytest.mark.run(order=14)
    @allure.story('同意')
    @allure.title('用例1')
    def test_2a(self, driver):
        allure.dynamic.description("动态description2")

        print(self.driver_find_element_and_wait(driver, *self.in_use).text)
        self.assert_FindElement(driver, *self.not_in_use)

    @allure.story('同意')
    @allure.title('用例2')
    def test_2b(self, driver):
        allure.dynamic.description("动态description3")

        print(self.driver_find_element_and_wait(driver, *self.in_use2).text)
        self.assert_FindElement(driver, *self.not_in_use)

    @pytest.mark.skip(reason='跳过')
    @allure.story('同意')
    @allure.title('用例3')
    def test_2c(self, driver):
        allure.dynamic.description("动态description4")

        print(self.driver_find_element_and_wait(driver, *self.in_use3).text)
        self.assert_FindElement(driver, *self.not_in_use)

    @allure.story('同意')
    @allure.title('用例4')
    def test_2d(self, driver):
        allure.dynamic.description("动态description5")

        print(self.driver_find_element_and_wait(driver, *self.in_use4).text)

        # 断言失败
        self.assert_FindElement(driver, *self.fail)

    @allure.story('同意')
    @allure.title('用例5')
    def test_3(self, driver):
        allure.dynamic.description("动态description6")

        self.driver_find_element_and_wait(driver, *self.in_use5).click()
        self.assert_FindElement(driver, *self.login)

    @allure.story('同意')
    @pytest.mark.run(order=13)
    @allure.title('用例6')
    def test_4(self, driver):
        allure.dynamic.description("动态description7")

        print(driver.current_activity)

    @allure.story('失败')
    @allure.title('用例1')
    def test_5(self, driver):
        allure.dynamic.description("动态description8")

        # 查找元素失败

        self.driver_find_element_and_wait(driver, *self.fail).click()
        self.assert_FindElement(driver, *self.fail)

    @pytest.mark.skip
    @allure.story('跳过')
    @allure.title('用例0')
    def test_6(self, driver):
        allure.dynamic.description("动态description9")

        print(driver.current_activity)

    @skip_mark
    @allure.story('跳过')
    @allure.title('用例1')
    def test_7a(self, driver):
        allure.dynamic.description("动态description10")

        print(driver.current_activity)

    @skip_mark
    @allure.story('跳过')
    @allure.title('用例2')
    def test_7b(self, driver):
        allure.dynamic.description("动态description")

        print(driver.current_activity)
        

File:conftest.py



@pytest.fixture(scope='function')
def driver():
    b = BaseDriver()
    device = 'phone_honor'
    dr = b.appium_desired(devices=device)
    yield dr
    b.driver_quit(dr)

结果展示

在这里插入图片描述

这一篇主要的内容就这些;

本系列 第二篇 生成测试报告,邮件、企微通知+Jenkins构建通知

交流技术 欢迎+QQ 153132336 zy
个人博客 https://blog.csdn.net/zyooooxie

猜你喜欢

转载自blog.csdn.net/zyooooxie/article/details/113841107
今日推荐