关于Selemium那些事

近期在看虫师编著的selenium 3自动化测试实战这本书,整体感觉这本书还是挺不错的,适用于一些初学者的入门书籍,它里面除了包含一些selenium相关的操作介绍,还浅谈了一下pytest和unittest这两个测试框,以及pom模型和appnium。不足的是,书中整个过程都是围绕百度首页的搜索功能为例,个人感觉实战性其实不是很强,另外就是在整本书中,并没有一个完整性的项目实例,对于一些selenium操作功能的讲解也不是非常深入。因为我之前已经了解过这块,并且还出了一篇博客,所以其实对于我个人来说参考价值不是很大,但整本书作为初学者的资料参考还是有一定的价值的。今天就给大家介绍下我这几天浅显的学习成果,希望各位能够介绍一些包含源码解析以及整体性实战项目的书籍给我

实践项目框架

在这里插入图片描述

project
    - base
        - 存放基础类
    - config
        - setting.py   配置日志、测试报告、测试数据存放的地址等信息
    - data
        - locatorData  存放定位数据
        - test_data    存放测试数据
    - logs
        - 存放生成日志
    - page
        - 页面元素定位即相关操作方法包
    - test_case
        - 测试用例包
    - test_report
        - 生成的测试报告
        - screenshots  失败截图
    - tools
        - loadData.py  加载读取文件数据
        - log.py  生成日志文件
        - sendMessage.py 发送邮件
    - conftest.py 本地测试配置文件

项目整体POM模型

Page Object Model , 页面对象模型 , 对页面进行抽象或者说建模的过程,是把一个具体的页面转化为编程语言中的一个对象,页面特性转化为
对象的属性,页面操作转化为对象的方法。主要用来实现页面操作和测试逻辑的一个分离, 将业务和实现分开,这样不仅提高了一些基础常用代码的
复用性, 也降低了代码间的耦合度, 使代码层级变得更加清晰,代码更易维护

本项目在base文件夹下创建了一个base_Page.py文件,其中创建了一个Tools类,该类封装了一些常用到的页面定位方法。在page下对每个具体的页面进行抽象化,一个页面测试点对应一个具体的类,这些类都继承了Tools方法,并且封装了当前测试点所用到的页面的特性定位和页面操作动作,为了实现更好地分离,页面定位数据均放在data.locatorData下的文件中,这样,一旦前端对页面元素进行了修改,我们只需要修改这些定位数据。而test_case下的测试用例只需要通过调用每个page类中便于识别的方法实现测试逻辑即可。

在这里插入图片描述

base_page.py

# 作者:yaxin.liang
# 日期:2022/9/13 10:43
# python版本:3.0
from selenium.webdriver import ActionChains
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait


class Tools:
    '''
    初始化浏览器
    :param  driver = webdriver.Chrome()
    '''
    def __init__(self,driver):
        self.driver = driver

    # 访问浏览器
    def open(self,url):
        self.driver.get(url)

    '''
    设置浏览器窗口的大小
    :param 不传则为None 即自适应浏览器界面大小
    '''
    def set_Windows_size(self,width=None,high=None):
        if width != None and high != None:
            self.driver.set_window_size(width,high)
        else:
            self.driver.maximize_window()


    # 元素定位 find_element(By.ID,"pwd")  *解包元组
    def locator(self,loc):
        try:
            a = WebDriverWait(self.driver, 10, 0.5).until(
                EC.visibility_of_element_located(loc)
            )
            return self.driver.find_element(*loc)
        except Exception as e:
            # print('页面中未找到 %s 元素'(loc))
            raise e

    # 定位一组元素
    def locators(self,loc):
        try:
            a = WebDriverWait(self.driver, 5, 0.5).until(
                EC.visibility_of_any_elements_located(loc)
            )
            return self.driver.find_elements(*loc)
        except Exception as e:
            # print('页面中未找到 %s 元素'(loc))
            raise e

    # 输入 定位后才能输入
    def input(self,loc,txt):
        self.locator(loc).send_keys(txt)

    # 转换frame
    def switch_frame(self,frame):
        self.driver.switch_to.frame(frame)

    # 点击
    def on_click(self,loc):
        self.locator(loc).click();

    # 执行 JavaScript 脚本
    def js(self,script):
        self.driver.execute_script(script)

    # 鼠标悬浮
    def actionPerform_move(self,loc):
        action = ActionChains(self.driver)
        action.move_to_element(self.locator(loc))
        action.perform()

    # 判断元素是否存在
    def isElementExist(self,loc):
        flag = True
        try:
            self.locator(loc)
            return flag
        except:
            flag = False
            return flag

    # 关闭
    def quit(self):
        self.driver.quit()

login_page_location.py

# 作者:yaxin.liang
# 日期:2022/10/8 13:53
# python版本:3.0

from selenium.webdriver.common.by import By

# 页面地址
url = "http://beta.kktv8.com/?pageId=100&actionid=10001"

# 定位首页登录按钮
login_link_id_local = (By.ID, "loginLink")
# 定位登录弹窗
login_frame_local = (By.XPATH, "/html/body/div[5]/div/div/div/div/iframe")
# 定位输入账号文本框
login_zhanghao_local = (By.XPATH, "/html/body/div[1]/div[1]/div/div[2]/div[1]/input")
# 定位输入密码文本框
login_password_local = (By.XPATH, "/html/body/div[1]/div[1]/div/div[2]/div[2]/input")
# 定位登录弹窗内的登录按钮
login_click_local = (By.XPATH, "/html/body/div[1]/div[1]/div/a")
# 定位登录成功后弹出的message提醒窗关闭按钮
login_message_click_local = (By.XPATH, "/html/body/div[6]/div/table/tbody/tr[3]/td/div[2]/button")
# 定位已登录头像 用来确定当前登录状态
login_sure_is_logined_local = (By.CSS_SELECTOR,
                         '#ReactWrap > div > div.nav_wrap.false > div > div > div.sub_wrap.clearfix > div.nav_right > ul > div > ul > li > a > img')
# 定位已登录的退出按钮
exit_local = (By.XPATH, '//*[@id="ReactWrap"]/div/div[1]/div/div/div[3]/div[2]/ul/div/ul/li/div/div/div[2]/div[2]/a')
# 定位登陆失败
login_sure_is_fail_local = (By.XPATH, '/html/body/div[1]/div[1]/div/div[6]/span/span[contains(text(),"请输入正确的账号和密码")]')
# 定位登录账号密码输入框清除按钮
login_zhanghao_clear_local = (By.XPATH, '/html/body/div[1]/div[1]/div/div[2]/div[1]/input')
login_password_clear_local = (By.XPATH, '/html/body/div[1]/div[1]/div/div[2]/div[2]/input')
# 定位退出登录弹窗按钮
login_iframe_exit_local = (By.XPATH, '/html/body/div[1]/div[1]/a')

# 定位登录成功后的用户头像信息
login_success_local = (By.XPATH, '//*[@id="ReactWrap"]/div/div[1]/div/div/div[3]/div[2]/ul/div/ul/li/a/img')

login_page.py

# 作者:yaxin.liang
# 日期:2022/9/13 11:00
# python版本:3.0


from selenium import webdriver

from myPractice.base.base_Page import Tools

# 八大定位元素:id、name、class_name、xpath、css_selector、tag_name、link_text、partial_link_text
from myPractice.tools.logger import atp_log
from myPractice.data.locatorData.login_page_location import *



class LoginPage(Tools):

    def get_login_page(self):
        atp_log.info("点击首页登录按钮,弹出登陆弹窗")
        ele = self.on_click(login_link_id_local)
        return ele

    def input_login_message(self,login_zhanghao_text,login_password_text):
        atp_log.info("输入登录账号密码")
        self.input(login_zhanghao_local,login_zhanghao_text)
        self.input(login_password_local,login_password_text)

    def local_frame(self):
        atp_log.info("定位登录弹窗")
        frame = self.locator(login_frame_local)
        return frame

    def local_exit_login(self):
        atp_log.info("退出登录弹窗")
        self.on_click(login_iframe_exit_local)

    def local_success_message(self):
        atp_log.info("定位登录成功后的信息")
        src = self.locator(login_success_local).get_attribute('src')
        return src

    def local_is_logined(self):
        atp_log.info("定位是否已经有账号登录")
        flag = self.isElementExist(login_sure_is_logined_local)
        return flag

    def local_and_exit(self):
        atp_log.info("正在退出当前登录")
        self.actionPerform_move(login_sure_is_logined_local)
        self.actionPerform_move((exit_local))
        self.on_click(exit_local)

    def local_login_button(self):
        atp_log.info("登录")
        self.on_click(login_click_local)

    def local_login_button_class_name(self):
        atp_log.info("检查登录按钮是否可点击")
        class_name = self.locator(login_click_local).get_attribute("class")
        return class_name

    def local_login_sure_is_fail(self):
        atp_log.info("定位是否登陆失败")
        flag = self.isElementExist(login_sure_is_fail_local)
        return flag

    def local_and_exit_login_message(self):
        # 判断是否弹出协议,是的话点击隐藏
        atp_log.info("关闭消息提醒弹窗")
        if self.isElementExist(login_message_click_local):
            self.on_click(login_message_click_local)

    def local_and_clear_login_message(self):
        atp_log.info("清除登录弹窗内的账号密码信息")
        if self.locator(login_zhanghao_local).text:
            self.on_click(self.locators(login_zhanghao_clear_local))
        if self.locator(login_password_local).text:
            self.on_click(self.locators(login_password_clear_local))


    def login(self,login_zhanghao_text,login_password_text):
        # 判断是否已经有账号登录
        if self.local_is_logined():
            atp_log.info("当前已登录!")
            self.local_and_exit()
            atp_log.info("已退出当前登录")
        else:
            atp_log.info("未登录!")
        self.get_login_page()
        frame = self.local_frame()
        self.switch_frame(frame)
        self.input_login_message(login_zhanghao_text,login_password_text)
        self.local_login_button()

test_case_login.py

# 作者:yaxin.liang
# 日期:2022/10/9 13:59
# python版本:3.0
import os
import time

import allure
import pytest
from selenium import webdriver

from myPractice.config.setting import url
from myPractice.conftest import browser, base_url
from myPractice.tools.common import loadYaml, loadJson, get_html_reports
from myPractice.page.login_page import LoginPage
from myPractice.tools.logger import atp_log

@allure.feature('登录')
class TestLogin:
    # 用例执行前要做的事
    def setup_class(cls) -> None:
        cls.driver = webdriver.Chrome()
        cls.driver.maximize_window()
        cls.lp = LoginPage(cls.driver)
        cls.lp.open(url)
        print('start setup_class...............')

    @allure.story('登录成功01')
    @allure.description('读取yaml文件数据')
    # # 读取yaml格式文件
    @pytest.mark.parametrize('utxt', loadYaml('../data/test_data/login.yaml'))
    def test_01(self, utxt):
        # driver = browser
        # lp = LoginPage(driver)
        # lp.open(base_url)
        try:
            self.lp.login(utxt['username'], utxt['pwd'])
            # 判断登陆结果是否正确
            scr = str(self.lp.local_success_message())
            assert utxt['username'] in scr
            self.lp.local_and_exit_login_message()
        except Exception as e:
            atp_log.info("TestLogin_test_01登录出错,具体报错:",e)
            assert False

    @allure.story('登录成功02')
    @allure.description('读取json文件数据')
    # 读取json格式文件
    def test_02(self):
        # 获取测试数据
        utxt = loadJson('../data/test_data/login.json')[0]
        # driver = browser
        # lp = LoginPage(driver)
        # lp.open(base_url)
        try:
            atp_log.info("开始对数据"+utxt['username']+":"+utxt['pwd']+"测试")
            self.lp.login(utxt['username'], utxt['pwd'])
            # 判断登陆结果是否正确
            scr = str(self.lp.local_success_message())
            assert utxt['username'] in scr
            atp_log.info("TestLogin_test_02登录成功")
            self.lp.local_and_exit_login_message()
        except Exception as e:
            atp_log.info("TestLogin_test_02登录出错,具体报错:", e)
            assert False


    # @allure.story('登陆失败01')
    # @allure.description('用户名错误,登陆失败')
    @pytest.mark.parametrize('utxt', loadJson('../data/test_data/login_fail.json'))
    def test_03(self,utxt):
        # driver = browser
        # lp = LoginPage(driver)
        # lp.open(base_url)
        story, description = utxt["story"], utxt["description"]
        allure.dynamic.story(story)
        allure.dynamic.description(description)
        try:
            atp_log.info("开始对数据" + utxt['username'] + ":" + utxt['pwd'] + "测试")
            self.lp.login(utxt['username'], utxt['pwd'])
            class_name = self.lp.local_login_button_class_name()
            # 判断登陆结果是否失败,失败则清除输入账号密码文本框,并关闭登录窗
            if class_name == 'back btn disabled':
                atp_log.info("登录按钮不可点击:" + class_name)
                assert True
            else:
                atp_log.info("登录按钮可点击")
                assert self.lp.local_login_sure_is_fail()
            atp_log.info("TestLogin_test_03登录失败,失败理由:"+ utxt["description"])
        except Exception as e:
            atp_log.info("TestLogin_test_03登录出错,具体报错:", e)
            assert False
        finally:
            self.lp.local_and_clear_login_message()
            self.lp.local_exit_login()

    # 用例执行后要做的事
    def teardown_class(cls) -> None:
        cls.driver.quit()
        atp_log.info("test 结束!")

Selenium基础

主流的web端UI自动化测试工具,支持多种编程语言,Selenium API提供了多种操作web的类和方法,Selenium Grid支持自动化测试的分布式执行。Selenium底层使用的是HTTP协议。

工作原理:在Selenium客户端使用Java、Python等语言编写操作指令,通过webDriver启动各个浏览器对象,并使用相应的API对浏览器对象进行操作,然后浏览器对象将响应结果传回到webDriver,由webDriver再传回到我们的客户端程序。在这个过程中,webDriver充当了一个代理服务器的角色。

八大元素定位

定位单个元素:find_element(定位方式,定位值)

定位一组元素:find_elements(定位方式,定位值)

- id 通过元素的唯一id值定位
	find_element_by_id("id值")
	find_element(By.ID, "id值")
- name 通过元素的name属性值定位
    find_element_by_name("name值")
    find_element(By.NAME, "name值")
- class 通过元素的class属性值定位
    find_element_by_class_name("class的值")
    find_element(By.CLASS_NAME, "class的值")
- tag 通过元素的标签名定位
    find_element_by_tag_name("tag的值")
    find_element(By.TAG_NAME, "tag的值")
- link 通过链接文本内容定位文本链接
    find_element_by_link_text("链接文本的值")
    find_element(By.LINK_TEXT, "链接文本的值")
- partial link 通过部分链接文本内容定位文本链接
    find_element_by_partial_link_text("部分链接文本的值")
    find_element(By.PARTIAL_LINK_TEXT, "部分链接文本的值")
- xpath 通过HTML规则进行定位
    - 绝对路径定位
    	find_element_by_xpath("xml文件规则的路径")
    	find_element(By.XPATH, "xml文件规则的路径")
    - 元素属性定位 元素需要是唯一
    	find_element(By.XPATH, "//元素的标签[@标签内属性='属性值']")
    	find_element(By.XPATH, "//*[@标签内属性='属性值']")
    - 层级与属性结合定位
    	find_element(By.XPATH, "//上一级元素标签/.../上一级元素标签[@标签内属性='属性值']/标签名")
    - 逻辑运算符定位
    	find_element(By.XPATH, "//元素标签名[@标签内属性='属性值' and @标签内属性='属性值']")
    	find_element(By.XPATH, "//元素标签名[@标签内属性='属性值' or @标签内属性='属性值']")
    - contains定位
    	find_element(By.XPATH, "//元素标签名[contains(@标签内属性,'属性值')]"
    - text定位
    	find_element(By.XPATH, "//标签名[text(),'text值']")
    	find_element(By.XPATH, "//标签名[contains(text(),'text值')]")
- css
    - class定位
    	find_element(By.CSS_SELECTOR, ".class的值")
    - id定位
    	find_element(By.CSS_SELECTOR, "#id的值")
    - 标签名定位
    	find_element(By.CSS_SELECTOR, "标签名")
    - 标签层级关系定位
    	find_element(By.CSS_SELECTOR, "上一级标签名 > 上一级标签名 > ... > 标签名")
    - 属性定位
    	find_element(By.CSS_SELECTOR, "[属性名='属性值']")
    - 组合定位
    	find_element(By.CSS_SELECTOR, "上一级标签名.标签名内的clas属性值 > 上一级标签名 > 上一级标签名#标签名内的id属性值 > 标签名.标签名内的clas属性值")
    - 更多定位
    	find_element(By.CSS_SELECTOR, "[属性*=包含的属性值")
    	find_element(By.CSS_SELECTOR, "[属性^=属性值开头的部分值]")
    	find_element(By.CSS_SELECTOR, "[属性$=属性值结尾的部分值]")
    	find_element(By.CSS_SELECTOR, "上一级标签名 > 标签名:nth-child(标签序列)")

浏览器控制

- 浏览器控制
    set_window_size('宽', '高') 设置浏览器窗口的大小
    set_maximize_window()  设置浏览器窗口全屏展示
    forward()   前进到新的网页
    back()  后退(返回)到前一个网页
    refresh()   刷新浏览器

常用方法

clear() 清除文本
send_keys('输入的内容value')    模拟按键输入
click() 单击元素
submit()    提交表单
size    返回元素尺寸
text    获取元素的文本
title   获取当前页面的标题
current_url 获取当前页面的URL
get_attribute('属性的name') 获取属性值
is_displayed()  判断该元素是否可见

鼠标操作

from selenium.webdriver import ActionChains
ActionChains(driver).方法(定位).perform()
- ActionChains方法列表
    click(on_element=None) 单击鼠标左键
    click_and_hold(on_element=None)	点击鼠标左键,不松开
    context_click(on_element=None) 点击鼠标右键
    double_click(on_element=None) 双击鼠标左键
    drag_and_drop(source, target) 拖拽到某个元素然后松开
    drag_and_drop_by_offset(source, xoffset, yoffset) 拖拽到某个坐标然后松开
    key_down(value, element=None) 按下某个键盘上的键
    key_up(value, element=None) 松开某个键
    move_by_offset(xoffset, yoffset) 鼠标从当前位置移动到某个坐标
    move_to_element(to_element) 鼠标移动到某个元素
    move_to_element_with_offset(to_element, xoffset, yoffset) 移动到距某个元素(左上角坐标)多少距离的位置
    perform()	执行链中的所有动作,即ActionChains类中存储的所有行为

    release(on_element=None) 在某个元素位置松开鼠标左键

    send_keys(*keys_to_send) 发送某个键到当前焦点的元素

    send_keys_to_element(element, *keys_to_send) 发送某个键到指定元素

键盘操作

from selenium.webdriver.common.Keys import Keys
send_keys(Keys.BACK_SPACE)  删除键
send_keys(Keys.SPACE)   空格键
send_keys(Keys.TAB) 制表键(Tab)
send_keys(Keys.ESCAPE)  回退键
send_keys(Keys.ENTER)   回车键
send_keys(Keys.CONTROL, 'a')    全选
send_keys(Keys.CONTROL, 'c')    复制
send_keys(Keys.CONTROL, 'x')    剪切
send_keys(Keys.CONTROL, 'v')    粘贴
send_keys(Keys.F1)  键盘F1

元素等待

显式等待:等待某个条件成立则继续执行,否则在达到最大时长时抛出异常,这个等待过程不会执行其它接下来的操作

# WebDriverWait()一般与until()或until_not()方法配合使用
WebDriverWait(driver, timeout='最长超长时间,秒单位', poll_frequency='检测的间隔时长,默认为0.5s', ignored_exceptions='超时后的异常').until(
    等待达到的预期条件判断方法
)
一般会使用expectd_conditions类提供的预期条件判断方法:
	title_is('title包含的预期value')    判断当前页的标题是否等于预期
	title_contains(title包含的的部分预期value)  判断当前页面的标题是否包含预期字符串
	presence_of_element_located('预期的元素定位') 判断元素是否被加在DOM树里,并不代表该元素一定可见
	visibility_of_element_located('预期的元素定位')   判断元素是否可见,可见不包括宽和高为0
	visibility_of('元素的预期value值')  判断元素是否可见,可见不包括宽和高为0
	presence_of_all_elements_located('预期的元素定位')    判断是否至少有一个元素存在于DOM树中
	text_to_be_present_in_element('text属性值包含的预期字符串')   判断某个元素的text是否包含预期的字符串
	text_to_be_precent_in_element_value('value属性值包含的预期字符串')   判断某个元素的value属性值是否包含预期的字符串
	frame_to_be_available_and_switch_to_it('预期的表单frame')    判断该表单是否可以切换进去,如果可以,则返回True并切换进去
	invisibility_of_element_located('元素的预期定位')  判断某个元素是否不在DOM树或不可见
	element_to_be_clickable('预期的元素定位') 判断某个元素是否可见并且可以点击
	staleness_of('预期的元素定位')  等到一个元素从DOM树中移除
	element_to_be_selected('预期的元素定位')    判断某个元素是否被选中,一般用在下拉列表中
	element_selection_state_to_be('预期定位后的元素') 判断某个元素的选中状态是否符合预期
	element_located_selection_state_to_be('预期的元素定位')   判断某个元素的选中状态是否符合预期
	alert_is_present()  判断页面是否存在alert
is_displayed()  判断元素是否存在

隐式等待:在达到最大超时时长前,对整个页面采用轮询的方式不断判断元素是否存在,等待过程中不影响接下来的操作执行,即在等待时会执行后面的操作。如果在在达到最大超时时长前定位到了元素,则在达到最大超时时长后继续执行,如果在达到最大超时时长时还未定位到元素,则抛出异常

driver.implicitly_wait('等待最大时长')

强制等待:即线程等待,通过线程休眠的方式完成等待,一般情况下不太使用强制等待,主要应用的场景在于不同系统交互的地方

Thread.sleep('等待时长,ms为单位')

主体切换

多表单切换

driver.switch_to.frame('frame定位')

多窗口切换

driver.current_window_handle 获得当前窗口句柄
window_handles	获得所有窗口的句柄到当前对话
switch_to.window('需要切换到的窗口的句柄') 切换到相应的窗口

警告框切换

# 切换到警告框
alert = switch_to.alert()	
# 获取alert、confirm、prompt中的文字信息
alert.text
# 接受现有警告框
alert.accept()
# 解散现有警告框
alert.dismiss()
# 在警告框中输入文本(可输入的话)
alert.send_keys('value')

下拉框处理

Select类用于定位<select>标签
Select('select定位').select_by_value('option的value属性值') 通过value值定位下拉选项
Select('select定位').select_by_visible_text('option的text值')	通过text值定位下拉选项
Select('select定位').select_by_index(索引)	通过下拉选项的索引进行选择,第一个选项的索引为0

文件上传

普通上传:将本地文件路径作为一个值放在input比安奇那种,通过form表单将这个值提交给服务器

file_path = os.path.abspath('./files/')
driver = webdriver.Chrome()
# 拼接测试的界面
upload_page = 'file:///' + file_path + 'upfile.html'
driver.get(upload_page)
# 定位上传按钮,添加本地文件
driver.find_element_by_id("file").send_keys(file_path + 'test.txt')

插件上传:一般是指基于Flask、JavaScript或Ajax等技术实现的上传功能

pass

文件下载

WebDriver允许设置默认的文件下载路径,文件会自动下载并且存放到设置的目录中

Firefox浏览器

import os
from selenium import webdriver

fp = webdriver.FirefoxProfile()
# browser.download.folderList 0 文件下载到浏览器默认下载路径 2 文件下载到指定文件路径
fp.set_preference("browser.download.folderList", 2)
# browser.download.dir 指定下载文件的目录
fp.set_preference("browser.download.dir", os.getcwd())
# 指定下载文件的类型  可参照 http://tool.oschina.net/commons
fp.set_preference("browser.helperApps.neverAsk.saveToDisk", "binary/octet-stream")

driver = webdriver.Firefox(firefox_profile = fp)
driver.get("https://pypi.org/project/selenium/#files")
driver.find_element_by_partial_link_text("selenium-3.141.0.tar.gz").click()

Chrome浏览器

import os
from selenium import webdriver

options = webdriver.ChromeOptions()
prefs = {
    # 设置为0 表示禁止弹出下载窗口
    'profile.default_content_settings.popups': 0,
    # 设置文件下载路径
    'download.default_directory': os.getcwd()
}
options.add_experimental_option('prefs', prefs)

driver = webdriver.Chrome(chrome_option = options)
driver.get("https://pypi.org/project/selenium/#files")
driver.find_element_by_partial_link_text("selenium-3.141.0.tar.gz").click()

操作cookie

get_cookies()	获取所有的Cookie
get_cookie(name)	返回字典中key为name的Cookie
add_cookie(cookie_dict)	添加Cookie
delete_cookie(name, optionsString)	删除名为optionsStrings的Cookie
delete_all_cookies()	删除所有的Cookie

调用JavaScript

# JavaScript语句
js = "window.scrollTo(100,450)"
# 执行JavaScript语句
driver.execute_script(js)

处理HTML5

# 返回播放文件地址
url = driver.execute_script("return arguments[0].currentSrc;", video)
# 加载视频
driver.execute_script(arguments[0].load())
# 播放视频
driver.execute_script("arguments[0].play()", video)
# 暂停视频
driver.execute_script("arguments[0].pause()", video)

滑动解锁

左右滑动

action = ActionChains(driver)
action.move_by_offset(x, y) 移动鼠标至坐标(x, y)
action.reset_action()	重置action
action.click_and_hold() 单击并按下鼠标左键

上下滑动

action = webdriver.TouchActions(driver)
action.scroll_from_element(on_element, xoffset, yoffset) 将目标元素on_element滑动到坐标(x, y)

窗口截图

driver.save_screenshot("截图图片保存的位置")

关闭窗口

driver.close()

数据读取

读取txt文件

def loadTxt(filename):
    '''
    :param filename: 传入的filename文本格式必须为
    a:b
    c:d
    :return: 返回的格式为二重数组
   [["a","b"],["c","d"]]
    '''
    # 读取文件
    with open(filename, "r") as f:
        txtData = f.readlines()
    # 格式化处理
    datas = []
    for line in txtData:
        data = line[:-1].split(":")
        datas.append(data)
    return datas

读取CSV文件

# 读取CSV文件
def loadCSV(filename):
    '''
    :param filename: filename的格式为
    标签名1 标签名2 标签名3
    a      b      c
    d      e      f
    :return: 返回数据格式
    [['a','b','c'],['d','e','f']]
    '''
    # codecs python标准的模块编码和解码器
    dataCSV = csv.reader(codecs.open(filename, 'r', 'utf_8_sig'))
    datas = []
    # python的内建模块itertools提供了用于操作迭代对象的函数islice(指定迭代对象, 指定开始迭代的位置, 结束位)
    for line in islice(dataCSV, 1, None):
        datas.append(line)
    return datas

读取XML文件

# 读取XML文件
def loadXML(filename, tagname=None, attributeName=None):
    '''

    :param filename: xml文件,标签形式
    :param tagname: 需要获得的标签名的值
    :return:使用tag_name[0].firstChild.data的形式接收
    '''
    # 打开XML文件
    dom = parse(filename)
    # 得到文档元素对象
    root = dom.documentElement
    # 获取一组标签
    tag_name = root.getElementsByTagName(tagname)
    # 获取属性值
    value= tag_name[0].getAttribute(attributeName)
    data = tag_name[0].firstChild.data
    return tag_name

读取JSON文件

# 读取json文件
def loadJson(filename):
    with open(filename, 'r', encoding='utf-8') as f:
        data = f.read()
    datas = json.loads(data)
    return datas

读取yaml文件

def loadYaml(filename):
    files = open(filename,'r',encoding='utf-8')
    # 读取files里面的内容
    data = yaml.load(files,Loader = yaml.FullLoader)
    print(data)
    files.close()
    return data

Unittest

规则

1、创建一个测试类,必须要继承unittest.TestCase类
2、创建的测试方法必须以"test"开头
3、unittest默认根据ASCII码的顺序加载测试用例,使用unittest.TestSuit().addTest('测试用例')可通过添加测试用例到suit的顺序来控制用例的
执行顺序

四大概念

1、Test Case : 最小的测试单元,提供了TestCase基类,创建的测试类需要机车给该类,它可以用来创建新的测试用例
2、Test Suit : 测试套件,是测试用例、测试太监或者两者的集合,用于组装一组需要运行的测试
3、Test Runer:一个用于协调测试的执行并向用户提供结果的组件。可以使用图形界面、文本界面或返回特殊值来展示执行测试的结果
4、Test Fixture: 代表执行一个或多个测试所需的环境准备,以及关联的清理动作

断言方法

方法检查
assertEqual(a, b)a = b
assertNotEqual(a, b)a != b
assertTrue(x)bool(x) is True
assertFalse(x)bool(x) is False
assertIs(a, b)a is b
assertIsNot(a, b)a is not b
assertIsNone(x)x is None
assertIsNotNone(x)x is not None
assertIn(a , b)a in b
assertNotIn(a, b)a not in b
assertIsInstance(a, b)is instance(a, b)
assertNotIsInstance(a, b)not is instance(a, b)

测试用例组织 discover方法

discover(start_dir='待测试的模块名或测试用例目录', pattern='测试用例文件名的匹配原则', top_level_dir='测试模块的顶层目录,默认为None')
import unittest

suit = unittest.defaultTestLoader.discover(setting.CASE_PATH,pattern='test_*_Unittest.py')
# 获取当前日期和时间
now_time = time.strftime("%Y-%m-%d %H-%M-%S")
html_reports = os.path.join(setting.REPORT_DIR + now_time + 'result.html')
fp = open(html_reports,'wb')
runner = HTMLTestRunner(stream=fp,
                        title="KK测试报告",
					  description="运行环境:Windows 10, Chrome 浏览器")
runner.run(suit)
fp.close()

跳过测试和预期失败

unittest.skip(reason)	无条件跳过测试用例
unittest.skipIf(condition, reason)	如果条件为真,则跳过装饰的测试
unittest.skipUnless(condition, reason)	如果条件为真时,执行装饰的测试
unittest.expectedFailure()	不管执行结果是否失败,都将测试标记为失败

Fixture

setUpModule/tearDownModule:在整个模块的开始与结束时被执行
setUpClass/tearDownClass:在测试类的开始与结束时被执行
setUp/tearDown:	在测试用例的开始与结束时被执行

pytest

Fixture

作用于模块级别和函数级别的Fixture:
setup_module/teardown_module:在当前文件中的所有测试用例执行之前和执行之后执行
setup_function/teardown_function:在每个测试函数执行之前和之后执行
setup/teardown:在每个测试函数之前和之后执行

作用于类级别和方法级别的Fixture
setup_class/teardown_class:在当前测试类的开始与结束时执行
setup_method/teardown_method:在每个测试方法执行前与执行后执行
setup/teardown:在每个测试方法执行前与执行后执行

conftest:是pytest特有的本地测试配置文件,既可以用来设置项目级别的Fixture,也可以用来导入外部插件,还可以用来指定钩子函数

失败截图配置

'''
@pytest.hookimpl(hookwrapper=True)装饰的钩子函数,有以下两个作用:
(1)可以获取到测试用例不同执行阶段的结果(setup,call,teardown)
(2)可以获取钩子方法的调用结果(yield返回一个result对象)和调用结果的测试报告(返回一个report对象)
'''

@pytest.mark.hookwrapper
def pytest_runtest_makereport(item,call):   # 对于给定的测试用例(item)和调用步骤(call),返回一个测试报告对象(_pytest.runner.TestReport)
    '''
    用于向测试用例中添加开始时间、内部注释和失败截图等
    :param item: 测试用例对象
    :param call:测试用例的测试步骤
           执行完常规钩子函数返回的report报告有个属性叫report.when
            先执行when=’setup’ 返回setup 的执行结果
            然后执行when=’call’ 返回call 的执行结果
    :return:
    '''
    pytest_html = item.config.pluginmanager.getplugin('html')
    print("pytest_html:",type(pytest_html))

    outcome = yield
    # report 包括 when、nodeid(测试用例名字)、outcome(用例的执行结果:passed,failed)
    report = outcome.get_result()
    report.description = str(item.function.__doc__)
    extra = getattr(report, 'extra', [])
    # 获取用例运行前和运行时的结果
    if report.when == 'call' or report.when == 'setup':
        # 用例失败
        xfail = hasattr(report, 'wasxfail')
        # if (report.skipped and xfail) or (report.failed and not xfail):
        if report.failed:
            # 将测试用例的“文件名+类名+方法名”作为截图的名称
            # 获取nodeid,并处理::,替换成_
            case_path = report.nodeid.replace("::","_") + ".png"
            print("case_path:",case_path)
            # if "[" in case_path:
            #     # 通过分隔符取第一个元素,避免出现-号,参数化叠加使用会出现
            #     case_name = case_path.split("-")[0] + "].png"
            # else:
            #     case_name = case_path
            case_name = case_path
            # 定义方法,实现本地截图
            image_dir = capture_screenshot(case_name)
            # img_path = image_dir + case_name.split("/")[-1]
            img_path = image_dir
            # 如果存在img_path,向pytest_html中添加截图网页代码
            if img_path:
                html = '<div><img scr="%s" alt="screenshot" style="width:304px;height:228px;" ' \
                        'οnclick="window.open(this.src)" align="right"/></div>'% img_path
                extra.append(pytest_html.extras.html(html))
        report.extra = extra

def capture_screenshot(case_name):
    '''
    # 配置测试用例失败截图路径
    # :param case_name: 用例名
    # :return:
    '''
    file_name = case_name.split("/")[-1]
    image_dir = os.path.join(setting.REPORT_DIR, "screenshots", file_name)
    ss = driver.get_screenshot_as_png()
    try:
        os.makedirs(os.path.dirname(image_dir), exist_ok=True)
        with open(image_dir, "wb") as  ss_file:
            ss_file.write(ss)
            print(f"Screenshot saved at {image_dir}")
    except Exception as err:
        print(f'Screenshot save failed with {type(err).__name__}: {err}')
    return image_dir

pytest-rerunfailures:测试用例失败时重试

pytest-parallel:实现测试用例的并行运行

数据驱动

Parameterized

unittest

from Parameterized import parameterized

@parameterized.expand([("case01", "selenium"),("case02", "unittest"),("case03", "parameterized")])
def test_search(self, name, search_key):
self.baidu_search(search_key)
self.assertEqual(self.driver.title, search_key + "_百度搜索")

pytest

@pytest.mark.parametrize(argnames,argvalues, indirect=False, ids=None, scope=None)
argnames:参数名
argvalues:参数对应值,可传多个值,类型必须为list [(values1,values2,…),(value1,value2,…)]
import pytest

'''
parametrize
'''

class Test02:
    def setup_class(self):
        print("---------setup_class----------")
    def teardown_class(self):
        print("------------teardown_class-------------")

    # 传递单参数
    @pytest.mark.parametrize("a",[3,6])
    def test_09(self,a):
        print("a = %d" % a)
        assert a%3 == 0

    # 传递多参数
    @pytest.mark.parametrize('a,b',[(0,3),[1,2]])
    def test_10(self,a,b):
        print("%d + %d = %d" % (a,b,a+b))
        assert a+b == 3

    def test_data():
        return [(1, 2), (0, 3)]

    # 函数返回值传递参数
    @pytest.mark.parametrize('a,b', test_data())
    def test_11(a, b):
        print("%d + %d = %d" % (a, b, a + b))
        assert a + b == 3

	def divsion(a,b):
    return int(a/b)

    @pytest.mark.parametrize('a,b,c',[(3,1,3),(0,2,0),(6,7,0)],ids=['整除','被除数为0','非整除'])
    def test_12(a,b,c):
        s = divsion(a,b)
        assert s == c
	
    # 叠加使用
    @pytest.mark.parametrize('a',[3,0,6])
    @pytest.mark.parametrize('b,c',[(1,3),(2,0),(7,0)])
    def test_13(a,b,c):
        s = divsion(a,b)
        assert s == c

DDT (针对unittest)

引入步骤:a、引入装饰器 @ddt b、导入数据的@data c、拆分数据的@unpack d、导入外部数据的@file_data

@ddt
class TestDDT01(unittest.TestCase):
    # ====================读取元组======================
    # 单组元素
    @data(1,2,3)
    def test01(self,value):
        print(value)

    # 未拆分多组元素
    @data((1,2,3),(4,5,6))
    def test02(self,value):
        print(value)

    # 拆分多组元素
    @data((1,2,3),(4,5,6))
    @unpack
    def test03(self,value1,value2,value3):
        print(value1,value2,value3)

    # ==================读取列表=========================
    # 多组元素未分解
    @data([{'name':'hahah','age':12},{'name':'heheh','age':18}])
    def test04(self,value):
        print(value)

    # 多组元素分解
    @data([{'name':'hahah','age':12},{'name':'heheh','age':18}])
    @unpack
    def test05(self,value1,value2):
        print(value1,value2)

    # ===================读取字典数========================
    # 多组元素未分解
    @data({'name': 'hahah', 'age': 12}, {'name': 'heheh', 'age': 18})
    def test06(self, value):
        print(value)

    # 多组数据拆分
    @data({'name': 'hahah', 'age': 12}, {'name': 'heheh', 'age': 18})
    @unpack
    # 在拆分的时候,形参和实参的key值要一致,否则就报错
    def test07(self, name, age):
        # 结果展示的数据是字典里的value,不会打印key的值
        print(name,age)

    # ===================读取文件数据========================
    testdata = [{'a': 'lili', 'b': 12}, {'a': 'sasa', 'b': 66}]
    @data(*testdata)
    # @unpack
    def test08(self, value):
        print(value)

    @file_data('yaml/login.yaml')
    def test10(self,**kwargs):
        print(kwargs.get('url'))

邮件发送

python自带发送功能

import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

from myPractice.config import setting
from myPractice.tools.logger import atp_log

# 使用python自带的发送邮件功能
def sendMessageByDefault(subject=None,attachment=None,contents=None):
    '''
    发送邮件
    :param subject:    邮件主题
    :param attachment: 附件
    :param msg:        邮件内容
    :param email:      邮箱类型
    :param sender:     发件人邮箱
    :param password:   发件人邮箱密码
    :param receiver:   收件人邮箱
    :return:
    '''
    try:
        # 附件
        with open(attachment,'rb') as f:
            send_attr = f.read()
        # 定义发送邮件的正文、格式以及编码
        att = MIMEText(send_attr,'text','utf-8')
        att["Content-Type"] = 'application/octet-stream'
        att['Content-Disposition'] = 'attachment; filename='+attachment

        msg = MIMEMultipart(contents)
        msg[subject] = Header(subject, 'utf-8')
        msg.attach(att)

        # 发送邮件
        smtp = smtplib.SMTP()
        smtp.connect(setting.MAIL_HOST)
        smtp.login(user=setting.MAIL_USER,password=setting.MAIL_PASSWRD)
        smtp.sendmail(from_addr=setting.MAIL_USER,to_addrs=setting.TO,msg=msg.as_string())
        atp_log.info("发送邮件完成!")
    except Exception as e:
        atp_log.error(e)
    finally:
        smtp.quit()

yagmail发送邮件

import yagmail

from myPractice.config import setting
from myPractice.tools.logger import atp_log

# 使用yagmail发送邮件
def sendMessageByYagmail(subject=None,attachments=None,contents=None):
    '''
    发送邮件
    :param subject:     邮件主题
    :param attachments: 附件  可为list传送多个
    :param contents:    邮件内容
    :param host:        邮箱类型
    :param user:        发件人邮箱
    :param password:    发件人邮箱密码
    :param receivers:   收件人邮箱  可为list发送给多个用户
    :return:
    '''

    try:
        # 连接邮箱服务器
        yag = yagmail.SMTP(user=setting.MAIL_USER, password=setting.MAIL_PASSWRD,host=setting.MAIL_HOST)
        # 发送邮件
        yag.send(to=setting.TO,subject=subject,contents=contents,attachments=attachments)
        atp_log.info("发送邮件完成!")
    except Exception as e:
        atp_log.error(e)

测试报告

HTML测试报告

unittest

import unittest

suit = unittest.defaultTestLoader.discover(setting.CASE_PATH,pattern='test_*_Unittest.py')
# 获取当前日期和时间
now_time = time.strftime("%Y-%m-%d %H-%M-%S")
html_reports = os.path.join(setting.REPORT_DIR + now_time + 'result.html')
fp = open(html_reports,'wb')
runner = HTMLTestRunner(stream=fp,
                        title="KK测试报告",
					  description="运行环境:Windows 10, Chrome 浏览器")
runner.run(suit)
fp.close()

pytest

if __name__ == "__main__":
    html_reports = get_html_reports()
    pytest.main(['-s', '-v', './test_case/test_case_By_Pytest.py', '--html='+html_reports])

Allure测试报告

Allure使用:
    @allure.feature()   模块名称
    @allure.story()     用例名称
    @allure.displayName() 用例标题
    @allure.issue()     缺陷地址
    @allure.description() 用例描述
    @allure.step()        操作步骤
    @allure.severity()    用例等级
    @allure.link()        定义链接
    @allure.addAttachment() 添加附件
    
# 作者:yaxin.liang
# 日期:2022/10/9 13:59
# python版本:3.0
import os
import time

import allure
import pytest
from selenium import webdriver

from myPractice.config.setting import url
from myPractice.conftest import browser, base_url
from myPractice.tools.common import loadYaml, loadJson, get_html_reports
from myPractice.page.login_page import LoginPage
from myPractice.tools.logger import atp_log

@allure.feature('登录')
class TestLogin:
    # 用例执行前要做的事
    def setup_class(cls) -> None:
        cls.driver = webdriver.Chrome()
        cls.driver.maximize_window()
        cls.lp = LoginPage(cls.driver)
        cls.lp.open(url)
        print('start setup_class...............')

    @allure.story('登录成功01')
    @allure.description('读取yaml文件数据')
    # # 读取yaml格式文件
    @pytest.mark.parametrize('utxt', loadYaml('../data/test_data/login.yaml'))
    def test_01(self, utxt):
        # driver = browser
        # lp = LoginPage(driver)
        # lp.open(base_url)
        try:
            self.lp.login(utxt['username'], utxt['pwd'])
            # 判断登陆结果是否正确
            scr = str(self.lp.local_success_message())
            assert utxt['username'] in scr
            self.lp.local_and_exit_login_message()
        except Exception as e:
            atp_log.info("TestLogin_test_01登录出错,具体报错:",e)
            assert False

    @allure.story('登录成功02')
    @allure.description('读取json文件数据')
    # 读取json格式文件
    def test_02(self):
        # 获取测试数据
        utxt = loadJson('../data/test_data/login.json')[0]
        # driver = browser
        # lp = LoginPage(driver)
        # lp.open(base_url)
        try:
            atp_log.info("开始对数据"+utxt['username']+":"+utxt['pwd']+"测试")
            self.lp.login(utxt['username'], utxt['pwd'])
            # 判断登陆结果是否正确
            scr = str(self.lp.local_success_message())
            assert utxt['username'] in scr
            atp_log.info("TestLogin_test_02登录成功")
            self.lp.local_and_exit_login_message()
        except Exception as e:
            atp_log.info("TestLogin_test_02登录出错,具体报错:", e)
            assert False


    # @allure.story('登陆失败01')
    # @allure.description('用户名错误,登陆失败')
    @pytest.mark.parametrize('utxt', loadJson('../data/test_data/login_fail.json'))
    def test_03(self,utxt):
        # driver = browser
        # lp = LoginPage(driver)
        # lp.open(base_url)
        story, description = utxt["story"], utxt["description"]
        allure.dynamic.story(story)
        allure.dynamic.description(description)
        try:
            atp_log.info("开始对数据" + utxt['username'] + ":" + utxt['pwd'] + "测试")
            self.lp.login(utxt['username'], utxt['pwd'])
            class_name = self.lp.local_login_button_class_name()
            # 判断登陆结果是否失败,失败则清除输入账号密码文本框,并关闭登录窗
            if class_name == 'back btn disabled':
                atp_log.info("登录按钮不可点击:" + class_name)
                assert True
            else:
                atp_log.info("登录按钮可点击")
                assert self.lp.local_login_sure_is_fail()
            atp_log.info("TestLogin_test_03登录失败,失败理由:"+ utxt["description"])
        except Exception as e:
            atp_log.info("TestLogin_test_03登录出错,具体报错:", e)
            assert False
        finally:
            self.lp.local_and_clear_login_message()
            self.lp.local_exit_login()

    # 用例执行后要做的事
    def teardown_class(cls) -> None:
        cls.driver.quit()
        atp_log.info("test 结束!")


if __name__ == "__main__":
    # 执行用例生成测试报告  测试数据  文件夹
    pytest.main(['-s', '-v', 'test_case_login.py', '--alluredir', '../test_report/allure/allure-results']);
    # 生成测试报告 测试数据
    os.system('allure generate ../test_report/allure/allure-results -o ../test_report/allure/allure-reports --clean')

在这里插入图片描述

在这里插入图片描述

完整代码

base/base_Page.py

# 作者:yaxin.liang
# 日期:2022/9/13 10:43
# python版本:3.0
from selenium.webdriver import ActionChains
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait


class Tools:
    '''
    初始化浏览器
    :param  driver = webdriver.Chrome()
    '''
    def __init__(self,driver):
        self.driver = driver

    # 访问浏览器
    def open(self,url):
        self.driver.get(url)

    '''
    设置浏览器窗口的大小
    :param 不传则为None 即自适应浏览器界面大小
    '''
    def set_Windows_size(self,width=None,high=None):
        if width != None and high != None:
            self.driver.set_window_size(width,high)
        else:
            self.driver.maximize_window()


    # 元素定位 find_element(By.ID,"pwd")  *解包元组
    def locator(self,loc):
        try:
            a = WebDriverWait(self.driver, 10, 0.5).until(
                EC.visibility_of_element_located(loc)
            )
            return self.driver.find_element(*loc)
        except Exception as e:
            # print('页面中未找到 %s 元素'(loc))
            raise e

    # 定位一组元素
    def locators(self,loc):
        try:
            a = WebDriverWait(self.driver, 5, 0.5).until(
                EC.visibility_of_any_elements_located(loc)
            )
            return self.driver.find_elements(*loc)
        except Exception as e:
            # print('页面中未找到 %s 元素'(loc))
            raise e

    # 输入 定位后才能输入
    def input(self,loc,txt):
        self.locator(loc).send_keys(txt)

    # 转换frame
    def switch_frame(self,frame):
        self.driver.switch_to.frame(frame)

    # 点击
    def on_click(self,loc):
        self.locator(loc).click();

    # 执行 JavaScript 脚本
    def js(self,script):
        self.driver.execute_script(script)

    # 鼠标悬浮
    def actionPerform_move(self,loc):
        action = ActionChains(self.driver)
        action.move_to_element(self.locator(loc))
        action.perform()

    # 判断元素是否存在
    def isElementExist(self,loc):
        flag = True
        try:
            self.locator(loc)
            return flag
        except:
            flag = False
            return flag

    # 关闭
    def quit(self):
        self.driver.quit()

config/setting.py

LEVEL = 'debug'
# 存放日志的路径,动态获取的,无论文件存放位置
LOG_PATH = os.path.join(BASE_PATH,'logs')
# 日志的文件名
LOG_NAME='atp.log'

# 测试报告存放的根目录
REPORT_DIR = BASE_PATH + "\\test_report\\"
# 新报告路径
NEW_REPORT = None



MAIL_HOST='smtp.163.com'
MAIL_USER='a18*****1*6*@163.com'
# 此处的密码是指163.com的授权码
MAIL_PASSWRD = '****DRXVV*****'
TO = [
    '26310*****@qq.com',
    'yaxin.liang@*****'
]

data/locatorData/login_page_location.py

# 作者:yaxin.liang
# 日期:2022/10/8 13:53
# python版本:3.0

from selenium.webdriver.common.by import By

# 页面地址
url = "http://***.kktv8.com/?pageId=100&actionid=10001"

# 定位首页登录按钮
login_link_id_local = (By.ID, "loginLink")
# 定位登录弹窗
login_frame_local = (By.XPATH, "/html/body/div[5]/div/div/div/div/iframe")
# 定位输入账号文本框
login_zhanghao_local = (By.XPATH, "/html/body/div[1]/div[1]/div/div[2]/div[1]/input")
# 定位输入密码文本框
login_password_local = (By.XPATH, "/html/body/div[1]/div[1]/div/div[2]/div[2]/input")
# 定位登录弹窗内的登录按钮
login_click_local = (By.XPATH, "/html/body/div[1]/div[1]/div/a")
# 定位登录成功后弹出的message提醒窗关闭按钮
login_message_click_local = (By.XPATH, "/html/body/div[6]/div/table/tbody/tr[3]/td/div[2]/button")
# 定位已登录头像 用来确定当前登录状态
login_sure_is_logined_local = (By.CSS_SELECTOR,
                         '#ReactWrap > div > div.nav_wrap.false > div > div > div.sub_wrap.clearfix > div.nav_right > ul > div > ul > li > a > img')
# 定位已登录的退出按钮
exit_local = (By.XPATH, '//*[@id="ReactWrap"]/div/div[1]/div/div/div[3]/div[2]/ul/div/ul/li/div/div/div[2]/div[2]/a')
# 定位登陆失败
login_sure_is_fail_local = (By.XPATH, '/html/body/div[1]/div[1]/div/div[6]/span/span[contains(text(),"请输入正确的账号和密码")]')
# 定位登录账号密码输入框清除按钮
login_zhanghao_clear_local = (By.XPATH, '/html/body/div[1]/div[1]/div/div[2]/div[1]/input')
login_password_clear_local = (By.XPATH, '/html/body/div[1]/div[1]/div/div[2]/div[2]/input')
# 定位退出登录弹窗按钮
login_iframe_exit_local = (By.XPATH, '/html/body/div[1]/div[1]/a')

# 定位登录成功后的用户头像信息
login_success_local = (By.XPATH, '//*[@id="ReactWrap"]/div/div[1]/div/div/div[3]/div[2]/ul/div/ul/li/a/img')

data/test_data/login.json

[
  {
    "username" : "10063828",
    "pwd" : "123456Aa",
    "story" : "登录成功01",
    "description" : "读取json文件数据"

  }
]

data/test_data/login.yaml

-
  username: "10063828"
  pwd: "123456Aa"
  allure_story: "登录成功01"
  allure_description: "读取yaml文件数据"

data/test_data/login_fail.json

[
  {
    "username" : "1006382890",
    "pwd" : "123456Aa",
    "story" : "登陆失败01",
    "description" : "用户名错误,登陆失败"
  },
  {
    "username" : "10063828",
    "pwd" : "123456",
    "story" : "登陆失败02",
    "description" : "密码错误,登陆失败"
  },
  {
    "username" : "",
    "pwd" : "",
    "story" : "登陆失败03",
    "description" : "用户和密码为空,登陆失败"
  },
  {
    "username" : "",
    "pwd" : "123456Aa",
    "story" : "登陆失败04",
    "description" : "用户为空,登陆失败"
  },
  {
    "username" : "10063828",
    "pwd" : "",
    "story" : "登陆失败05",
    "description" : "密码为空,登陆失败"
  },
  {
    "username" : "~@#¥%……&*()?、’;",
    "pwd" : "文@#¥%…",
    "story" : "登陆失败06",
    "description" : "非法字符串,登陆失败"
  }
]

page/login_page.py

# 作者:yaxin.liang
# 日期:2022/9/13 11:00
# python版本:3.0


from selenium import webdriver

from myPractice.base.base_Page import Tools

# 八大定位元素:id、name、class_name、xpath、css_selector、tag_name、link_text、partial_link_text
from myPractice.tools.logger import atp_log
from myPractice.data.locatorData.login_page_location import *



class LoginPage(Tools):

    def get_login_page(self):
        atp_log.info("点击首页登录按钮,弹出登陆弹窗")
        ele = self.on_click(login_link_id_local)
        return ele

    def input_login_message(self,login_zhanghao_text,login_password_text):
        atp_log.info("输入登录账号密码")
        self.input(login_zhanghao_local,login_zhanghao_text)
        self.input(login_password_local,login_password_text)

    def local_frame(self):
        atp_log.info("定位登录弹窗")
        frame = self.locator(login_frame_local)
        return frame

    def local_exit_login(self):
        atp_log.info("退出登录弹窗")
        self.on_click(login_iframe_exit_local)

    def local_success_message(self):
        atp_log.info("定位登录成功后的信息")
        src = self.locator(login_success_local).get_attribute('src')
        return src

    def local_is_logined(self):
        atp_log.info("定位是否已经有账号登录")
        flag = self.isElementExist(login_sure_is_logined_local)
        return flag

    def local_and_exit(self):
        atp_log.info("正在退出当前登录")
        self.actionPerform_move(login_sure_is_logined_local)
        self.actionPerform_move((exit_local))
        self.on_click(exit_local)

    def local_login_button(self):
        atp_log.info("登录")
        self.on_click(login_click_local)

    def local_login_button_class_name(self):
        atp_log.info("检查登录按钮是否可点击")
        class_name = self.locator(login_click_local).get_attribute("class")
        return class_name

    def local_login_sure_is_fail(self):
        atp_log.info("定位是否登陆失败")
        flag = self.isElementExist(login_sure_is_fail_local)
        return flag

    def local_and_exit_login_message(self):
        # 判断是否弹出协议,是的话点击隐藏
        atp_log.info("关闭消息提醒弹窗")
        if self.isElementExist(login_message_click_local):
            self.on_click(login_message_click_local)

    def local_and_clear_login_message(self):
        atp_log.info("清除登录弹窗内的账号密码信息")
        if self.locator(login_zhanghao_local).text:
            self.on_click(self.locators(login_zhanghao_clear_local))
        if self.locator(login_password_local).text:
            self.on_click(self.locators(login_password_clear_local))


    def login(self,login_zhanghao_text,login_password_text):
        # 判断是否已经有账号登录
        if self.local_is_logined():
            atp_log.info("当前已登录!")
            self.local_and_exit()
            atp_log.info("已退出当前登录")
        else:
            atp_log.info("未登录!")
        self.get_login_page()
        frame = self.local_frame()
        self.switch_frame(frame)
        self.input_login_message(login_zhanghao_text,login_password_text)
        self.local_login_button()



if __name__ == "__main__":
    driver = LoginPage(webdriver.Chrome());
    # 设置浏览器窗口展示的大小  宽,高,默认适应
    driver.set_Windows_size()
    driver.open(url)
    # driver.maxmize_window()
    driver.login("","");
    driver.local_login_button_class_name()


tools/common.py

# 作者:yaxin.liang
# 日期:2022/9/13 16:06
# python版本:3.0
import codecs
import csv
import json
import os
import time
from itertools import islice
from xml.dom.minidom import parse

import yaml

# 读取yaml文件
from myPractice.config import setting


def loadYaml(filename):
    files = open(filename,'r',encoding='utf-8')
    # 读取files里面的内容
    data = yaml.load(files,Loader = yaml.FullLoader)
    print(data)
    files.close()
    return data


# 读取json文件
def loadJson(filename):
    with open(filename, 'r', encoding='utf-8') as f:
        data = f.read()
    datas = json.loads(data)
    return datas

if __name__ == "__main__":
    loadYaml("../data/test_data/login.json")

# 读取excel文件
def loadExcel(filename):
    pass


# 读取XML文件
def loadXML(filename, tagname=None, attributeName=None):
    '''

    :param filename: xml文件,标签形式
    :param tagname: 需要获得的标签名的值
    :return:使用tag_name[0].firstChild.data的形式接收
    '''
    # 打开XML文件
    dom = parse(filename)
    # 得到文档元素对象
    root = dom.documentElement
    # 获取一组标签
    tag_name = root.getElementsByTagName(tagname)
    # 获取属性值
    value= tag_name[0].getAttribute(attributeName)
    data = tag_name[0].firstChild.data
    return tag_name


# 读取CSV文件
def loadCSV(filename):
    '''
    :param filename: filename的格式为
    标签名1 标签名2 标签名3
    a      b      c
    d      e      f
    :return: 返回数据格式
    [['a','b','c'],['d','e','f']]
    '''
    # codecs python标准的模块编码和解码器
    dataCSV = csv.reader(codecs.open(filename, 'r', 'utf_8_sig'))
    datas = []
    # python的内建模块itertools提供了用于操作迭代对象的函数islice(指定迭代对象, 指定开始迭代的位置, 结束位)
    for line in islice(dataCSV, 1, None):
        datas.append(line)
    return datas


def loadTxt(filename):
    '''
    :param filename: 传入的filename文本格式必须为
    a:b
    c:d
    :return: 返回的格式为二重数组
   [["a","b"],["c","d"]]
    '''
    # 读取文件
    with open(filename, "r") as f:
        txtData = f.readlines()
    # 格式化处理
    datas = []
    for line in txtData:
        data = line[:-1].split(":")
        datas.append(data)
    return datas


# 写入文件

# 获取报告
def get_html_reports():
    time_dir = time.strftime("%Y-%m-%d %H-%M")
    html_reports = os.path.join(setting.REPORT_DIR, time_dir,'result.html')
    return html_reports

tools/logger.py

# 作者:yaxin.liang
# 日期:2022/9/14 14:04
# python版本:3.0
import logging
import os
import time
from logging import handlers

from myPractice.config import setting


class MyLogger():
    def __init__(self,file_name,level='info',backCount=5,when='D'):
        logger = logging.getLogger()
        logger.setLevel(self.get_level(level))
        cl = logging.StreamHandler()
        bl = logging.handlers.TimedRotatingFileHandler(filename=file_name, when=when, interval=1, backupCount=backCount, encoding='utf-8')
        fmt = logging.Formatter('%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s')
        cl.setFormatter(fmt)
        bl.setFormatter(fmt)
        logger.addHandler(cl)
        logger.addHandler(bl)
        self.logger = logger

    def get_level(self,str):
        level = {
            'debug': logging.DEBUG,
            'info': logging.INFO,
            'warn': logging.WARNING,
            'error': logging.ERROR
        }
        str = str.lower()
        return level.get(str)

path = os.path.join(setting.LOG_PATH,setting.LOG_NAME+time.strftime("%Y-%m-%d %H-%M")) #拼好日志的绝对路径
atp_log = MyLogger(path,setting.LEVEL).logger  #日志级别
#直接在这里实例化,用的时候就不用再实例化了

tools/sendMessage.py

# 作者:yaxin.liang
# 日期:2022/9/14 9:53
# python版本:3.0
import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import yagmail


# 使用python自带的发送邮件功能
from myPractice.config import setting
from myPractice.tools.logger import atp_log


def sendMessageByDefault(subject=None,attachment=None,contents=None):
    '''
    发送邮件
    :param subject:    邮件主题
    :param attachment: 附件
    :param msg:        邮件内容
    :param email:      邮箱类型
    :param sender:     发件人邮箱
    :param password:   发件人邮箱密码
    :param receiver:   收件人邮箱
    :return:
    '''
    try:
        # 附件
        with open(attachment,'rb') as f:
            send_attr = f.read()
        # 定义发送邮件的正文、格式以及编码
        att = MIMEText(send_attr,'text','utf-8')
        att["Content-Type"] = 'application/octet-stream'
        att['Content-Disposition'] = 'attachment; filename='+attachment

        msg = MIMEMultipart(contents)
        msg[subject] = Header(subject, 'utf-8')
        msg.attach(att)

        # 发送邮件
        smtp = smtplib.SMTP()
        smtp.connect(setting.MAIL_HOST)
        smtp.login(user=setting.MAIL_USER,password=setting.MAIL_PASSWRD)
        smtp.sendmail(from_addr=setting.MAIL_USER,to_addrs=setting.TO,msg=msg.as_string())
        atp_log.info("发送邮件完成!")
    except Exception as e:
        atp_log.error(e)
    finally:
        smtp.quit()


# 使用yagmail发送邮件
def sendMessageByYagmail(subject=None,attachments=None,contents=None):
    '''
    发送邮件
    :param subject:     邮件主题
    :param attachments: 附件  可为list传送多个
    :param contents:    邮件内容
    :param host:        邮箱类型
    :param user:        发件人邮箱
    :param password:    发件人邮箱密码
    :param receivers:   收件人邮箱  可为list发送给多个用户
    :return:
    '''

    try:
        # 连接邮箱服务器
        yag = yagmail.SMTP(user=setting.MAIL_USER, password=setting.MAIL_PASSWRD,host=setting.MAIL_HOST)
        # 发送邮件
        yag.send(to=setting.TO,subject=subject,contents=contents,attachments=attachments)
        atp_log.info("发送邮件完成!")
    except Exception as e:
        atp_log.error(e)

conftest.py

# 作者:yaxin.liang
# 日期:2022/9/14 15:06
# python版本:3.0

# 测试配置文件
import os
import time
from functools import wraps

from decorator import decorator
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options as CH_Options
from selenium.webdriver.firefox.options import Options as FF_Options
from selenium.webdriver.remote.webdriver import RemoteConnection

# 配置浏览器驱动类型
from myPractice.config import setting
from myPractice.tools.logger import atp_log

driver_type = "chrome"
driver = webdriver.Chrome()

@pytest.fixture()
def base_url():
    url = "http://***.kktv8.com/?pageId=100&actionid=10001"
    return url

# 启动浏览器
@pytest.fixture(scope='session', autouse=True)
def browser():
    '''
    定义全局浏览器驱动
    :return:
    '''

    global driver
    global driver_type

    if driver_type == "chrome":
        # 本地Chrome浏览器
        driver = webdriver.Chrome()
        driver.maximize_window()

    elif driver_type == "firefox":
        # 本地FireFox浏览器
        driver = webdriver.Firefox()
        driver.maximize_window()

    elif driver_type == "chrome-headless":
        # chrome headless模式
        chrome_options = CH_Options()
        # 将Chrome设置成可视化无界面模式
        chrome_options.add_argument("--headless")
        # 加上这个属性来避免bug
        chrome_options.add_argument('--disable-gpu')
        # 设置浏览器分辨率
        chrome_options.add_argument("--window-size=1920*1080")
        # 解决DeToolsActivePort文件不存在的报错
        chrome_options.add_argument("--no-sandbox")
        driver = webdriver.Chrome(chrome_options=chrome_options)

    elif driver_type == "firefox-headless":
        # firefox headless模式
        firefox_options = FF_Options()
        firefox_options.headless = True
        driver = webdriver.Firefox(firefox_options=firefox_options)

    else:
        raise NameError("driver 驱动类型定义错误!")
    atp_log.info("进入browser选择driver!")

    return driver

# 关闭浏览器
@pytest.fixture(scope='session', autouse=True)
def browser_close():
    yield driver
    driver.quit()
    atp_log.info("test 结束!")



'''
@pytest.hookimpl(hookwrapper=True)装饰的钩子函数,有以下两个作用:
(1)可以获取到测试用例不同执行阶段的结果(setup,call,teardown)
(2)可以获取钩子方法的调用结果(yield返回一个result对象)和调用结果的测试报告(返回一个report对象)
'''

@pytest.mark.hookwrapper
def pytest_runtest_makereport(item,call):   # 对于给定的测试用例(item)和调用步骤(call),返回一个测试报告对象(_pytest.runner.TestReport)
    '''
    用于向测试用例中添加开始时间、内部注释和失败截图等
    :param item: 测试用例对象
    :param call:测试用例的测试步骤
           执行完常规钩子函数返回的report报告有个属性叫report.when
            先执行when=’setup’ 返回setup 的执行结果
            然后执行when=’call’ 返回call 的执行结果
    :return:
    '''
    pytest_html = item.config.pluginmanager.getplugin('html')
    print("pytest_html:",type(pytest_html))

    outcome = yield
    # report 包括 when、nodeid(测试用例名字)、outcome(用例的执行结果:passed,failed)
    report = outcome.get_result()
    report.description = str(item.function.__doc__)
    extra = getattr(report, 'extra', [])
    # 获取用例运行前和运行时的结果
    if report.when == 'call' or report.when == 'setup':
        # 用例失败
        xfail = hasattr(report, 'wasxfail')
        # if (report.skipped and xfail) or (report.failed and not xfail):
        if report.failed:
            # 将测试用例的“文件名+类名+方法名”作为截图的名称
            # 获取nodeid,并处理::,替换成_
            case_path = report.nodeid.replace("::","_") + ".png"
            print("case_path:",case_path)
            # if "[" in case_path:
            #     # 通过分隔符取第一个元素,避免出现-号,参数化叠加使用会出现
            #     case_name = case_path.split("-")[0] + "].png"
            # else:
            #     case_name = case_path
            case_name = case_path
            # 定义方法,实现本地截图
            image_dir = capture_screenshot(case_name)
            # img_path = image_dir + case_name.split("/")[-1]
            img_path = image_dir
            # 如果存在img_path,向pytest_html中添加截图网页代码
            if img_path:
                html = '<div><img scr="%s" alt="screenshot" style="width:304px;height:228px;" ' \
                        'οnclick="window.open(this.src)" align="right"/></div>'% img_path
                extra.append(pytest_html.extras.html(html))
        report.extra = extra

def capture_screenshot(case_name):
    '''
    # 配置测试用例失败截图路径
    # :param case_name: 用例名
    # :return:
    '''
    file_name = case_name.split("/")[-1]
    image_dir = os.path.join(setting.REPORT_DIR, "screenshots", file_name)
    ss = driver.get_screenshot_as_png()
    try:
        os.makedirs(os.path.dirname(image_dir), exist_ok=True)
        with open(image_dir, "wb") as  ss_file:
            ss_file.write(ss)
            print(f"Screenshot saved at {image_dir}")
    except Exception as err:
        print(f'Screenshot save failed with {type(err).__name__}: {err}')
    return image_dir



if __name__ == "__main__":
    pass

test_case/test_case_by_unittest.py

# 作者:yaxin.liang
# 日期:2022/9/14 9:41
# python版本:3.0

import time
import unittest

from ddt import ddt,file_data
from selenium import webdriver

from myPractice.page.login_page import LoginPage

@ddt
class TestLogin(unittest.TestCase):
    # 用例执行要做的事
    @classmethod
    def setUp(cls) -> None:
        # 会启动一个chromedriver.exe进程
        cls.driver = webdriver.Chrome();
        cls.lp = LoginPage(cls.driver);
        cls.lp.set_Windows_size()


    @file_data('../data/login.yaml')
    def test_01(self,username,pwd):
        self.lp.login(username,pwd);
        time.sleep(3);

    @file_data('../data/login.json')
    def test_02(self, username, pwd):
        self.lp.login(username, pwd);
        time.sleep(3);


    # 用例执行后要做的事
    @classmethod
    def tearDown(cls) -> None:
        cls.driver.quit();


if __name__ == "__main__":
    unittest.main();


test_case/test_case_login.py

# 作者:yaxin.liang
# 日期:2022/10/9 13:59
# python版本:3.0
import os
import time

import allure
import pytest
from selenium import webdriver

from myPractice.config.setting import url
from myPractice.conftest import browser, base_url
from myPractice.tools.common import loadYaml, loadJson, get_html_reports
from myPractice.page.login_page import LoginPage
from myPractice.tools.logger import atp_log

@allure.feature('登录')
class TestLogin:
    # 用例执行前要做的事
    def setup_class(cls) -> None:
        cls.driver = webdriver.Chrome()
        cls.driver.maximize_window()
        cls.lp = LoginPage(cls.driver)
        cls.lp.open(url)
        print('start setup_class...............')

    @allure.story('登录成功01')
    @allure.description('读取yaml文件数据')
    # # 读取yaml格式文件
    @pytest.mark.parametrize('utxt', loadYaml('../data/test_data/login.yaml'))
    def test_01(self, utxt):
        # driver = browser
        # lp = LoginPage(driver)
        # lp.open(base_url)
        try:
            self.lp.login(utxt['username'], utxt['pwd'])
            # 判断登陆结果是否正确
            scr = str(self.lp.local_success_message())
            assert utxt['username'] in scr
            self.lp.local_and_exit_login_message()
        except Exception as e:
            atp_log.info("TestLogin_test_01登录出错,具体报错:",e)
            assert False

    @allure.story('登录成功02')
    @allure.description('读取json文件数据')
    # 读取json格式文件
    def test_02(self):
        # 获取测试数据
        utxt = loadJson('../data/test_data/login.json')[0]
        # driver = browser
        # lp = LoginPage(driver)
        # lp.open(base_url)
        try:
            atp_log.info("开始对数据"+utxt['username']+":"+utxt['pwd']+"测试")
            self.lp.login(utxt['username'], utxt['pwd'])
            # 判断登陆结果是否正确
            scr = str(self.lp.local_success_message())
            assert utxt['username'] in scr
            atp_log.info("TestLogin_test_02登录成功")
            self.lp.local_and_exit_login_message()
        except Exception as e:
            atp_log.info("TestLogin_test_02登录出错,具体报错:", e)
            assert False


    # @allure.story('登陆失败01')
    # @allure.description('用户名错误,登陆失败')
    @pytest.mark.parametrize('utxt', loadJson('../data/test_data/login_fail.json'))
    def test_03(self,utxt):
        # driver = browser
        # lp = LoginPage(driver)
        # lp.open(base_url)
        story, description = utxt["story"], utxt["description"]
        allure.dynamic.story(story)
        allure.dynamic.description(description)
        try:
            atp_log.info("开始对数据" + utxt['username'] + ":" + utxt['pwd'] + "测试")
            self.lp.login(utxt['username'], utxt['pwd'])
            class_name = self.lp.local_login_button_class_name()
            # 判断登陆结果是否失败,失败则清除输入账号密码文本框,并关闭登录窗
            if class_name == 'back btn disabled':
                atp_log.info("登录按钮不可点击:" + class_name)
                assert True
            else:
                atp_log.info("登录按钮可点击")
                assert self.lp.local_login_sure_is_fail()
            atp_log.info("TestLogin_test_03登录失败,失败理由:"+ utxt["description"])
        except Exception as e:
            atp_log.info("TestLogin_test_03登录出错,具体报错:", e)
            assert False
        finally:
            self.lp.local_and_clear_login_message()
            self.lp.local_exit_login()

    # 用例执行后要做的事
    def teardown_class(cls) -> None:
        cls.driver.quit()
        atp_log.info("test 结束!")


if __name__ == "__main__":
    '''
    1、生成JUnit XML文件报告
    pytest -s -v test_case_By_Pytest.py --junit-xml=./test_report/log.xml
    2、生成在线测试报告
    pytest -s -v test_case_By_Pytest.py --pastebin=all
    3、生成HTML格式测试报告
    pytest -s -v test_case_By_Pytest.py --html=./test_report/result.xml
    '''

    # pytest.main(['-s', '-v', 'test_case_By_Pytest.py'])
    # 执行用例生成测试报告  测试数据  文件夹
    pytest.main(['-s', '-v', 'test_case_login.py', '--alluredir', '../test_report/allure/allure-results']);
    # 生成测试报告 测试数据
    os.system('allure generate ../test_report/allure/allure-results -o ../test_report/allure/allure-reports --clean')
    # html_reports = get_html_reports()
    # pytest.main(['-s', '-v', 'test_case_login.py', '--html=' + html_reports])

run_by_pytest.py

# 作者:yaxin.liang
# 日期:2022/9/13 16:26
# python版本:3.0


# 生成测试报告 pytest
# allure 测试报告
'''
环境准备:
1、安装allure_pytest  pip install allure-pytest
2、下载commanline包  下载到指定位置 python下
3、配置环境变量 找到path: C:\python\python38\allure-2.13.5\bin
4、重启
'''
import os
from XTestRunner import HTMLTestRunner

import pytest

from myPractice.tools.common import  get_html_reports

if __name__ == "__main__":
    # 执行用例生成测试报告  测试数据  文件夹
    # pytest.main(['-s','-v','./test_case/test_case_By_Pytest02.py','--alluredir','./allure-results'])
    html_reports = get_html_reports()
    pytest.main(['-s', '-v', './test_case/test_case_By_Pytest.py', '--html='+html_reports])
    # 生成测试报告 测试数据
    # os.system('allure generate ./allure-results -o ./allure-reports --clean')

run_by_unittest.py

# 作者:yaxin.liang
# 日期:2022/9/14 10:37
# python版本:3.0
import os
import time
import unittest
from XTestRunner import HTMLTestRunner

from myPractice.config import setting
from myPractice.tools.sendMessage import sendMessageByYagmail



if __name__ == "__main__":
    # 获取测试套件
    suit = unittest.defaultTestLoader.discover(setting.CASE_PATH,pattern='test_*_Unittest.py')

    # 获取当前日期和时间
    now_time = time.strftime("%Y-%m-%d %H-%M-%S")
    html_reports = os.path.join(setting.REPORT_DIR + now_time + 'result.html')
    fp = open(html_reports,'wb')
    runner = HTMLTestRunner(stream=fp,
                            title="KK测试报告",
                            description="运行环境:Windows 10, Chrome 浏览器")
    runner.run(suit)
    fp.close()
    # 发送邮件
    sendMessageByYagmail(subject='KK测试报告',
                         attachments=html_reports)


  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值