企业微信 web 自动化测试实战

霍格沃兹测试开发学社 ceshiren.com

目录

  • 成果展示
  • Selenium 知识点梳理
  • PO 讲解
  • 实战演练

课程准备

成果展示

  • 工程结构
  • 运行效果

企业微信产品介绍

  • 企业通讯与办公工具
  • 有很丰富的 OA 应用

Selenium 学习路线

Selenium 知识点梳理

  • xmind 思维导图梳理知识点
    • Selenium WebDriver
    • Selenium IDE
    • Selenium Grid

用例设计:手工测试与自动化测试对比

UI 自动化技术

  • Web 自动化测试: Selenium、Cypress、Airtest
  • App 自动化测试: Appium、ATX、Airtest

自动化测试用例设计

  • 参照手工用例怎么设计思路
自动化场景:企业微信添加成员(web端)

1. 登录
2. 进入首页页面
3. 点击"添加成员"按钮
4. 填写成员信息
5. 点击"保存"按钮
6. 进入通讯录页面
7. 验证==>断言

Selenium 实现添加联系人用例-面条式

  • 环境
    • Python: v3.10.x
    • Pytest: v7.x.x
    • Selenium: v4.x.x
# test_login.py 登录
"""
@Author: 霍格沃兹测试开发学社-西西
@Desc: 更多测试开发技术探讨,请访问:https://ceshiren.com/t/topic/15860
"""
import time

import yaml
from selenium import webdriver

# 企业微信的cookie 有互踢机制。
class TestCookieLogin:

    def setup_class(self):
        """前置动作"""
        self.driver = webdriver.Chrome()

    def teardown_class(self):
        """后置处理"""
        pass
        # self.driver.quit()

    def test_save_cookies(self):
        """获取cookie"""

        # 1、访问企业微信首页
        self.driver.get("https://work.weixin.qq.com/wework_admin/frame#index")

        # 2、直接等待,手工扫码
        time.sleep(10)

        # 3、登录成功后,获取cookie
        cookies = self.driver.get_cookies()

        # 4、保存cookie
        with open("./data/cookies.yaml", "w") as f:
            yaml.safe_dump(data=cookies, stream=f)

    def test_add_cookie(self):
        """植入cookie"""

        # 1、访问企业微信首页 CookieDomain
        self.driver.get("https://work.weixin.qq.com/wework_admin/frame#index")

        # 2、获取本地 cookies
        with open("./data/cookies.yaml", "r") as f:
            cookies = yaml.safe_load(f)

        # 3、植入cookies
        for ck in cookies:
            self.driver.add_cookie(ck)

        # 4、访问企业微信首页
        self.driver.get("https://work.weixin.qq.com/wework_admin/frame#index")

# test_contact.py 添加联系人
"""
@Author: 霍格沃兹测试开发学社-西西
@Desc: 更多测试开发技术探讨,请访问:https://ceshiren.com/t/topic/15860
"""
import yaml
from faker import Faker
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait

from utils.log_util import logger


class TestAddMemberFromHome:
    def setup_class(self):
        # mock 数据 姓名,account, 手机号
        fake:Faker = Faker("zh_CN")
        self.username = fake.name()
        self.accid = fake.ssn()
        self.mobile = fake.phone_number()
        # 实例化
        self.driver = webdriver.Chrome()
        self.driver.implicitly_wait(5)
        self.driver.maximize_window()
        logger.info("登录")
        # 1、访问企业微信首页
        self.driver.get("https://work.weixin.qq.com/wework_admin/frame")
        # 2、获取本地的cookie
        with open("./data/cookies.yaml", "r") as f:
            cookies = yaml.safe_load(f)
        # 3、植入cookie
        for ck in cookies:
            self.driver.add_cookie(ck)
        # 4、访问企业微信首页
        self.driver.get("https://work.weixin.qq.com/wework_admin/frame")

    def teardown_class(self):
        # 不要退出
        # self.driver.quit()
        pass

    def test_add_member(self):
        # name = "aaab"
        # acctid = "111112"
        # phonenum = "13100000001"
        # 2. 点击添加成员按钮
        logger.info("点击添加成员按钮")
        self.driver.find_element(By.CSS_SELECTOR, ".ww_indexImg_AddMember").click()
        # 3. 填写成员信息
        logger.info("填写成员信息")
        # 3.1 输入用户名
        self.driver.find_element(By.ID, "username").send_keys(self.username)
        # 3.2 输入acctid
        self.driver.find_element(By.ID, "memberAdd_acctid").send_keys(self.accid)

        # 3.3 输入手机号
        self.driver.find_element(By.ID, "memberAdd_phone").send_keys(self.mobile)

        # 3.4 点击保存
        self.driver.find_element(By.CSS_SELECTOR, ".js_btn_save").click()
        # 4. 断言结果
        loc_tips = (By.ID, "js_tips")
        # 等到可见,再去获取结果文字
        WebDriverWait(self.driver,10,2).until(expected_conditions.visibility_of_element_located(loc_tips))
        tips_value = self.driver.find_element(*loc_tips).text
        assert "保存成功" == tips_value

    def test_delete_contact(self):
        # 删除刚添加的联系人
        self.driver.find_element(By.XPATH, "//*[text()='通讯录']").click()
        self.driver.find_element(By.XPATH,"//span[text()='汪文']/../..//input").click()
        self.driver.find_element(By.XPATH, "//*[text()='删除']").click()
        self.driver.find_element(By.XPATH, "//*[@d_ck='submit_hr_helper']").click()
        loc_tips = (By.ID, "js_tips")
        # 等到可见,再去获取结果文字
        WebDriverWait(self.driver, 10, 2).until(expected_conditions.visibility_of_element_located(loc_tips))
        print(f"冒泡消息:{loc_tips}")

传统代码痛点

  • 大段重复代码
  • 无法适应 UI 变化

PageObject 设计思想

PO 设计思想

  • 分工

  • 页面 ==> 类

    • 属性(名词):元素
    • 方法(动词):功能

PO 原则解读

  • 属性意义

    • 不要暴露页面内部的元素给外部
    • 不需要建模 UI 内的所有元素
  • 方法意义

    • 用公共方法代表 UI 所提供的功能
    • 方法应该返回其他的 PageObject 或者返回用于断言的数据
    • 同样的行为不同的结果可以建模为不同的方法
    • 不要在方法内加断言

企业微信PO建模

原型图

企业微信PO建模

  • 方块代表一个类
  • 每条线代表这个页面提供的方法
  • 箭头的始端为开始页面
  • 箭头的末端为跳转页面或需要断言的数据

企业微信 PO 封装过程

企业微信实战演练

企业微信实战:PO与链式调用

  • PageObject:编写PO
  • 测试用例:实现链式调用

企业微信实战:定位与断言

  • PageObject:融入元素定位
  • 测试用例:实现断言

企业微信实战:封装 BasePage

  • driver 对象的实例化

企业微信实战:封装元素定位

  • 常用的 UI 操作封装在 base_page 中

企业微信实战:总结

框架优化

  • 报错截图并保存 page source 信息(装饰器实现)
  • 添加日志(hook)
  • 数据清理(添加完用例清理数据)
  • 参数化
  • 添加测试报告数据

清理数据-删除联系人

  • 用例添加删除
  • PO 中实现删除封装

# test_contact.py
def test_addcontact(self):
    """添加成员"""
    contact_list = self.browser.login().click_add_member().\
        edit_contact(self.username, self.accid,self.mobile)
    tips = contact_list.get_tips()
    assert tips=='保存成功'
    # 删除联系人
    contact_list.del_operate(self.username)

# contactlist_page.py  通讯录页面PO封装
class ContactListPage(BasePage):
    _TIPS_LOC = (By.ID, "js_tips")
    _INPUT_USERNAME = By.XPATH, "//span[text()='{name}']/../..//input"
    _BNT_DELETE = By.XPATH, "//*[text()='删除']"
    _BNT_DELLETE_SUBMIT = By.XPATH, "//*[@d_ck='submit_hr_helper']"
    def get_tips(self):
        """获取提示文本"""
        # 等到可见,再去获取结果文字
        self.wait_element_until_visible(self._TIPS_LOC)
        tips_value = self.get_text(*self._TIPS_LOC)
        return tips_value

    def del_operate(self, name):
        # 删除刚添加的联系人
        self.find_click(self._INPUT_USERNAME[0], self._INPUT_USERNAME[1].format(name=name))
        self.find_click(*self._BNT_DELETE)
        self.find_click(*self._BNT_DELLETE_SUBMIT)

添加日志

  • 知识点:pytest 直播课-hook 部分
  • 知识点:web 自动化测试 L2-自动化关键数据记录
# conftest.py
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
    # 获取钩子方法的调用结果
    out = yield
    # 从钩子方法的调用结果中获取测试报告
    report = out.get_result()
    fail_case_info = []
    # 错误类型
    error_type = None
    # 错误信息
    error_msg = None
    # 错误完整信息
    error_longrepr = None
    # 如果用例执行不通过
    if report.outcome != "passed":
        # 如果运行时有相应的错误日志则捕获日志,赋值到一个变量中
        if call.excinfo:
            error_type = call.excinfo.typename
            error_msg = call.excinfo.value.msg
            error_longrepr = str(out._result.longrepr)

        case_info = {
            "nodeid": report.nodeid,
            "result": report.outcome,
            "type": error_type,
            "msg": error_msg,
            "longrepr": error_longrepr
        }
        fail_case_info.append(case_info)
        # 用例信息写入 yaml 文件
        with open('../fail_record/fail_cases_info.yaml', 'a', encoding='utf-8') as f:
            yaml.dump(fail_case_info, f)

        logger.error(f'错误类型 =>> {error_type},\n'
                     f'错误信息 =>> {error_msg},\n'
                     f'错误详情 =>> {error_longrepr} \n')

扩展三 - 异常截图+ 保存 page source 源码

- 知识点:web 自动化测试 L3-自动化关键数据记录
- 如果未找到元素,则
  - 保存截图
  - 同时保存页面源码
  - 同时保存日志
- 如果找到元素,则不截图也不存 page source
# utils/except_handler.py 异常处理
def ui_exception_record(func):
    def run(*args, **kwargs):
        self = args[0]
        try:
            return func(*args, **kwargs)
        except Exception as e:
            # 这里添加所有的异常情况处理
            # 日志
            logger.warning("执行过程中发生异常")
            # 截图
            timestamp = int(time.time())
            image_path = f"./images/"
            image_file = image_path + f"image_{timestamp}.PNG"
            page_source_path = f"./page_source/"
            page_source_file =page_source_path + f"{timestamp}_page_source.html"
            # page_source
            if not os.path.exists(page_source_path):
                os.makedirs(page_source_path)
            with open(page_source_file, "w", encoding="u8") as f:
                f.write(self.driver.page_source)
            if not os.path.exists(image_path):
                os.makedirs(image_path)
            self.driver.save_screenshot(image_file)
            allure.attach.file(image_path, name="image", attachment_type=allure.attachment_type.PNG)
            # allure.attach.file(page_source_path, name="page_source", attachment_type=allure.attachment_type.HTML)
            allure.attach.file(page_source_path, name="page_source", attachment_type=allure.attachment_type.TEXT)
            raise e
    return run

# 将装饰器装饰到 base_page.py 文件的 `do_find()` 方法上,在查找元素的时候,如果未找到,则报错,并保存截图和pagesource
# base_page.py
@ui_exception_record
def do_find(self, by, locator=None):
    """查找元素"""
    logger.info(f"查找元素:by:{by}, locator:{locator}")
    if locator:
        return self.driver.find_element(by, locator)
    else:
        return self.driver.find_element(*by)