Web UI 自动化的分层设计思想

项目目录结构

│  conftest.py
│  run.py
│  setting.py
│
├─cases
│  │  test_attendance.py
│  │  test_login.py
│  │  __init__.py
│  │
├─common
│  │  basepage.py
│  │  __init__.py
├─data
│  │  login_data.py
│  │  __init__.py
├─pages
│  │  course_page.py
│  │  home_page.py
│  │  login_page.py
│  │  __init__.py
├─report
│      040e1a67-05be-48d2-ac48-d9c5d564fc3a-container.json
│      05615248-db84-44a5-ae2a-0bb4faf7096b-container.json
└─

基础方法层 common

basepage.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2022/1/22 12:48
# @Author  : shisuiyi
# @File    : basepage.py
# @Software: win10 Tensorflow1.13.1 python3.9
import time

import allure
from selenium.webdriver import ActionChains, Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait



class BasePage:
    """储存浏览器,页面的通用操作"""

    def __init__(self, driver):
        self.driver = driver

    def goto(self, url):
        self.driver.get(url)

    def reload(self):
        """刷新"""
        self.driver.refresh()

    def wait_element_clickable(self, locator, timeout=10):
        """等待元素可以被点击"""
        wait = WebDriverWait(self.driver, timeout=timeout, poll_frequency=0.2)
        el = wait.until(EC.element_to_be_clickable(locator))
        return el

    def wait_element_visible(self, locator, timeout=10):
        """等待元素可见"""
        wait = WebDriverWait(self.driver, timeout=timeout, poll_frequency=0.2)
        el = wait.until(EC.visibility_of_element_located(locator))
        return el

    def wait_title_is(self, title, timeout=10):
        """等待 title 等于"""
        wait = WebDriverWait(self.driver, timeout=timeout, poll_frequency=0.2)
        # EC.title_is(title) 内置的显性等待,页面的标题是否等于
        return wait.until(EC.title_is(title))

    def wait_url_contains(self, url, timeout=10):
        """等待 url包含"""
        wait = WebDriverWait(self.driver, timeout=timeout, poll_frequency=0.2)
        return wait.until(EC.url_contains(url))

    def get_element(self, locator):
        """查找元素"""
        el = self.driver.find_element(*locator)
        return el

    def get_elements(self, locator):
        """查找多个元素"""
        elements = self.driver.find_elements(*locator)
        time.sleep(1)
        return elements

    def write(self, locator, value):
        """输入操作"""
        el = self.driver.find_element(*locator)
        el.send_keys(value)
        return self  # return self返回的是类的实例。

    def click(self, locator):
        """鼠标点击。方法2"""
        el = self.wait_element_clickable(locator)
        #  el = self.driver.find_element(*locator)
        action = ActionChains(self.driver)
        action.click(el).perform()

    def double_click(self, locator):
        el = self.wait_element_clickable(locator)
        #  el = self.driver.find_element(*locator)
        action = ActionChains(self.driver)
        action.double_click(el).perform()

    def context_click(self, locator):
        el = self.driver.find_element(*locator)
        action = ActionChains(self.driver)
        action.context_click(el).perform()

    def click_and_hold(self, locator):
        """长按"""
        try:
            el = self.driver.find_element(*locator)
            action = ActionChains(self.driver)
            action.click_and_hold(el).perform()
        except Exception as e:
            print('[ERROR]-长按页面元素{}失败,原因:{}'.format(locator, e))

    def clear_element(self, locator):
        """元素清空"""
        self.wait_element_clickable(locator).clear()
        return self

    def select(self, locator1, locator2):
        """下拉选择"""
        self.click(locator1)
        self.click(locator2)

    def enter(self):
        """回车"""
        action = ActionChains(self.driver)
        action.send_keys(Keys.ENTER).perform()

    def send_file(self, locator, file_path):
        """发送文件"""
        el = self.wait_element_visible(locator)
        el.send_keys(file_path)

    def move_to(self, locator):
        """鼠标悬停, locator = ('xpath', 'value')"""
        el = self.driver.find_element(*locator)
        action = ActionChains(self.driver)
        action.move_to_element(el).perform()

    def drag_and_drop(self, locator_start, locator_end):

        start_el = self.wait_element_clickable(locator_start)
        end_el = self.wait_element_clickable(locator_end)
        action = ActionChains(self.driver)
        action.drag_and_drop(start_el, end_el).perform()

    def switch_to_iframe(self, iframe_reference, timeout=30):
        """iframe切换"""
        # self.driver.switch_to.frame(iframe_reference)
        wait = WebDriverWait(self.driver, timeout=timeout, poll_frequency=0.2)
        wait.until(EC.frame_to_be_available_and_switch_to_it(iframe_reference))
        # frame_to_be_available_and_switch_to_it 此方法会判断iframe是否可用,并且会自动切换到iframe中。

    def allure_screenshot(self, name=None):
        """截图"""
        f = self.driver.get_screenshot_as_png()
        return allure.attach(f,
                             name=name,
                             attachment_type=allure.attachment_type.PNG)

    def script(self, src):
        """
        定义script方法,用于执行js脚本
        """
        self.driver.execute_script(src)

    def switch_window(self, n):
        """窗口切换"""
        # WebDriver对象有window_handles 属性,这是一个列表对象, 里面包括了当前浏览器里面所有的窗口句柄。
        self.driver.switch_to.window(self.driver.window_handles[n])

    def assert_element_attribute_equal(self, locator, attr_name, expected):
        """断言元素的text文本等于"""
        el = self.wait_element_visible(locator)
        actual = el.get_attribute(attr_name)
        print("文本", actual)
        print(expected)
        assert actual == expected

元素定位层Pages

course_page.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2022/1/25 20:19
# @Author  : shisuiyi
# @File    : course_page.py
# @Software: win10 Tensorflow1.13.1 python3.9

from common.basepage import BasePage
from time import sleep


class CoursePage(BasePage):
    # 元素定位
    # 考勤
    attendance_locator = ('xpath', '//span[text()="考勤"]')
    # iframe
    iframe_locator = ('id', 'layui-layer-content1')
    # 新建考勤
    new_attendance_locator = ('link text', '新建考勤')
    # 数字考勤
    number_attendance_locator = ('xpath', '//span[@class="lable-title" and text()="数字考勤"]')
    # 开始考勤
    start_attendance_locator = ('xpath', '//a[text()="开始考勤"]')
    # 获取考勤码
    attendance_code_locator = ('xpath', '//div[@class="number-box"]/span')
    # 考勤人数
    student_number_locator = ('xpath', '//i[@class="ing"]')
    # 结束考勤
    finish_attendance_locator = ('xpath', '//a[text()="结束"]')
    # 确认结束
    confirm_finish_attendance_locator = ('xpath', '(//a[text()="结束"])[5]')

    # 学生端立即签到
    right_sign_locator = ('link text', '立即签到')
    # 输入签到码
    sign_code_locator = ('id', 'phoneVer_modalAuthInput')

    def add_attendance(self):
        """
        1,课程详情页, 点击 “考勤”   //span[text()="考勤"]/..
        2, 切换 iframe ,   id = layui-layer-content1
        3, 点击 “新建考勤”   link text 新建考勤
        4, 点击  数字考勤 ,   //span[@class="lable-title" and text()="数字考勤"]
        5, 点击 “开始考勤”  ,  //a[text()="开始考勤"]
        6, 获取考勤码,  find_elements //div[@class="number-box"]/span ,
        el = [],  el.text
        """
        self.click(self.attendance_locator)
        sleep(3)
        # 切换 iframe
        self.switch_to_iframe(self.iframe_locator)
        # 点击新建考勤
        self.click(self.new_attendance_locator)
        # 数字考勤
        self.click(self.number_attendance_locator)
        # 开始考勤
        self.click(self.start_attendance_locator)
        # 获取考勤码的四个元素
        self.wait_element_visible(self.attendance_code_locator)
        number_elements = self.get_elements(self.attendance_code_locator)
        # numbers = []
        numbers = [el.text for el in number_elements]
        code = ''.join(numbers)
        return code

    def student_sign(self, code):
        """
        1,立即签到    link text 立即签到
        2,输入签到码  id phoneVer_modalAuthInput     code
        """
        self.click(self.right_sign_locator)
        self.write(self.sign_code_locator, code)

    def get_student_number(self):
        """获取考勤人数"""
        el = self.get_element(self.student_number_locator)
        return el.text

    def end_attendance(self):
        """
        # 老师端
        1,结束     //a[text()="结束"]
        2,再点击确认结束  (//a[text()="结束"])[5]
        """
        self.click(self.finish_attendance_locator)
        self.click(self.confirm_finish_attendance_locator)

home_page.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2022/1/21 20:43
# @Author  : shisuiyi
# @File    : home_page.py
# @Software: win10 Tensorflow1.13.1 python3.9

from common.basepage import BasePage


class HomePage(BasePage):
    # 头像元素
    avatar_locator = ('xpath', "//img[@class='avatar']")

    def get_username(self, expected):
        """获取用户名称"""
        self.assert_element_attribute_equal(self.avatar_locator, 'alt', expected)

    def enter_course_page(self, course_name):
        """选择进入的课程"""
        self.click(("link text", course_name))

login_page.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2022/1/20 20:34
# @Author  : shisuiyi
# @File    : login_page.py
# @Software: win10 Tensorflow1.13.1 python3.9
from selenium.webdriver.common.by import By

import setting
from common.basepage import BasePage


class LoginPage(BasePage):
    """登录页面"""
    # 通过类属性获取元素的定位方式与元素定位表达式,便于修改与管理。
    url = setting.host + '/User/login.html'
    mobile_locator = (By.XPATH, '//input[@name="account"]')
    password_locator = (By.XPATH, '//input[@name="pass"]')
    login_btn_locator = (By.XPATH, '//a[@class="btn-btn" and text()="登录"]')
    error_msg = (By.XPATH, '//p[@class="error-tips"]')

    def login(self, *args):
        """登录"""
        # 输入用户户名
        # self.driver.find_element(By.XPATH, '//input[@name="account"]').send_keys(args[0])
        self.write(self.mobile_locator, args[0])
        # 输入密码
        # self.driver.find_element(By.XPATH, '//input[@name="pass"]').send_keys(args[1])
        self.write(self.password_locator, args[1])
        # 点击登录
        # self.driver.find_element(By.XPATH, '//a[@class="btn-btn" and text()="登录"]').click()
        self.click(self.login_btn_locator)

    def logout(self):
        """退出登录"""
        pass

    def forget_password(self):
        """忘记密码"""
        pass

    def get_error_msg(self):
        """获取登录失败的错误信息"""
        return self.get_element(self.error_msg).text

测试数据层

login_data.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2022/1/21 20:58
# @Author  : shisuiyi
# @File    : login_data.py
# @Software: win10 Tensorflow1.13.1 python3.9
data_error = [['123', 'abc', '密码有效长度是6到30个字符'],
              ['123', '12345678', '用户名或密码无效'],
              ['188****7463', '1234568', '密码错误']]

data_success = [('188****7463', '********205f', '师语'), ]

测试用例层cases

test_login.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2022/1/13 19:46
# @Author  : shisuiyi
# @File    : test_login.py
# @Software: win10 Tensorflow1.13.1 python3.9
import pytest
from data.login_data import data_error, data_success
from pages.login_page import LoginPage
from pages.home_page import HomePage


class TestLogin(object):
    """测试登录功能"""

    @pytest.mark.parametrize('username,password,expected', data_error, ids=['密码过短', '用户名或密码无效', '密码错误'])
    def test_login_error(self, username, password, expected, driver):
        login_page = LoginPage(driver)
        login_page.goto(url=LoginPage.url)
        login_page.login(username, password)
        actual = login_page.get_error_msg()
        assert expected in actual

    @pytest.mark.parametrize('username,password,expected', data_success, ids=['登录成功'])
    def test_login_success(self, username, password, expected, driver):
        login_page = LoginPage(driver)
        login_page.goto(url=LoginPage.url)
        login_page.login(username, password)
        HomePage(driver).get_username(expected)

test_attendance.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2022/1/24 20:43
# @Author  : shisuiyi
# @File    : test_attendance.py
# @Software: win10 Tensorflow1.13.1 python3.9
"""
1, 不要着急写代码
2, 梳理测试的步骤(手工测试)
3, 准备测试数据, 在哪个页面的操作, 元素定位表达式, 用户名,密码。
前置,后置的
4, 代码
"""
import time
import pytest


from pages.course_page import CoursePage
from pages.home_page import HomePage


@pytest.mark.attendance
class TestAttendance:
    def test_attendance(self, teacher_driver, student_driver):
        """
        老师: 学生:
        1、老师访问登录页面,完成登录操作 https://v4.ketangpai.com/User/login.html
        2、首页, 点击课程详情,  进入课程详情页,  a   python九歌
        3,课程详情页, 点击 “考勤”   //span[text()="考勤"]/..
        4, 切换 iframe ,   id = layui-layer-content1
        5, 点击 “新建考勤”   link text 新建考勤
        6, 点击  数字考勤 ,   //span[@class="lable-title" and text()="数字考勤"]
        7, 点击 “开始考勤”  ,  //a[text()="开始考勤"]
        8, 获取考勤码,  find_elements //div[@class="number-box"]/span ,
        el = [],  el.text

        # 学生端
        1, 学生登录
        2, 进入课程页面   a   python九歌
        3, 立即签到    link text 立即签到
        4,  输入签到码  id phoneVer_modalAuthInput     code

        # 老师端
         //i[@class="ing"]   text =   1
         结束     //a[text()="结束"]
         再点击确认结束  (//a[text()="结束"])[5]
        :return:
        """
        time.sleep(2)
        # 老师登录加入课程页面,选择课程python九歌
        HomePage(teacher_driver).enter_course_page('python九歌')
        teacher_course_page = CoursePage(teacher_driver)
        code = teacher_course_page.add_attendance()

        # 学生进入课程页面,选择课程python九歌
        time.sleep(2)
        HomePage(student_driver).enter_course_page('python九歌')
        student_course_page = CoursePage(student_driver)
        student_course_page.student_sign(code)
        # 强制等待,学生签到之后,消息队列(任务)
        time.sleep(4)
        actual = teacher_course_page.get_student_number()
        assert actual == '1'
        # 老师结束考勤
        teacher_course_page.end_attendance()

测试脚手架

conftest.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2022/1/15 14:14
# @Author  : shisuiyi
# @File    : conftest.py
# @Software: win10 Tensorflow1.13.1 python3.9
import pytest
from selenium import webdriver
from pages.login_page import LoginPage


def pytest_collection_modifyitems(items):
    """
    测试用例收集完成时,将收集到的item的name和nodeid的中文显示在控制台上,
    为解决使用 pytest.mark.parametrize 参数化的时候,加 ids 参数用例描述有中文时,
    在控制台输出会显示unicode编码,中文不能正常显示。
    :return:
    """
    for item in items:
        item.name = item.name.encode("utf-8").decode("unicode_escape")
        print(item.nodeid)
        item._nodeid = item.nodeid.encode("utf-8").decode("unicode_escape")


def get_driver():
    """获取浏览器"""
    driver = webdriver.Chrome()
    # 养成设置隐性
    driver.implicitly_wait(8)
    # 窗口最大化
    driver.maximize_window()
    return driver


@pytest.fixture(scope='session')
def driver():
    """管理浏览器的夹具"""
    d = get_driver()
    yield d
    d.quit()


@pytest.fixture(scope='session')
def teacher_driver():
    """老师的浏览器"""
    d = get_driver()
    LoginPage(d).goto(url=LoginPage.url)
    LoginPage(d).login('*******.com', 'ad****6')
    yield d
    d.quit()


@pytest.fixture(scope='session')
def student_driver():
    """学生的浏览器"""
    d = get_driver()
    LoginPage(d).goto(url=LoginPage.url)
    LoginPage(d).login('*******.com', 'ad****6')
    yield d
    d.quit()

运行测试

run.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2022/1/12 20:41
# @Author  : shisuiyi
# @File    : run.py
# @Software: win10 Tensorflow1.13.1 python3.9
import pytest

pytest.main(['--alluredir=report'])