Python的ui自动化框架搭建

目录


目录

一、web项目框架搭建

1.1 项目结构

web自动化框架和接口自动化框架一致,使用分层设计。

在项目根目录下新建common文件夹,用来存储公用方法。

在项目根目录下新建reports文件夹,用来存储项目报告。

在项目根目录下新建logs文件夹,用来存储项目日志。

在项目根目录下新建test_data文件夹,用来存储用例数据。

在项目根目录下新建test_cases文件夹,用例存储测试用例模块。

在项目根目录下新建main.py文件,作为项目入口,方便项目调试。

在项目根目录下新建setting.py文件,作为配置文件。

1.2 代码

1.2.1 项目配置文件

在setting.py文件中配置项目信息,内容如下:

import os
​
# 项目根目录
BASE_DIR=os.path.dirname(os.path.abspath(__file__))
​
# 测试用例路径
TEST_CASE_DIR=os.path.join(BASE_DIR,'test_cases')
​
# 项目域名
PROJECT_HOST='http://testingedu.com.cn:8000'
​
​
# url信息
INTERFACE={
    'login':'/Home/User/login.html'
}
​
# 日志配置
LOG_CONFIG={
    'name':'test123',
    'filename':os.path.join(BASE_DIR,'logs/test123.log'),
    'mode':'a',
    'encoding':'utf-8',
    'debug':True
}
​
# 测试账户信息
TEST_NORMAL_USERNAME = '13800138006'
TEST_NORMAL_PASSWORD = '123456'

1.2.2 入口文件编写

main.py文件内容如下:

(需安装好allure环境,具体步骤可参考http://t.csdn.cn/Dguft中生成报告模块)

import pytest
import settings
​
if __name__ == '__main__':
    pytest.main(['-s','-v','--alluredir=./reports',settings.TEST_CASE_DIR])

1.2.3 日志模块内容编写

日志模块复用接口的日志模块log_handler.py,

在common文件夹下新建log_handler.py,内容如下:

import logging
​
def get_logger(name,filename,mode='a',encoding='utf-8',fmt=None,debug=False):
    '''
​
    :param name: 日志器的名字
    :param filename:日志文件名
    :param mode:文件模式
    :param encoding:字符编码
    :param fmt:日志格式
    :param debug:调试模式
    :return:
    '''
​
    #创建一个日志器并设置日志等级
    logger=logging.getLogger(name)
    logger.setLevel(logging.DEBUG)
    #确定文件和控制台输出的日志级别,文件处理器的等级一般情况比控制台要高
    if debug:
        file_level=logging.DEBUG
        console_level=logging.DEBUG
    else:
        file_level=logging.WARNING
        console_level=logging.INFO
​
    #定义日志的输出格式
    if fmt is None:
        fmt='%(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d] - %(message)s'
​
    #创建日志处理器
    #写入文件的日志处理器
    file_handler=logging.FileHandler(filename=filename,mode=mode,encoding=encoding)
    file_handler.setLevel(file_level)
    #写入控制台的日志处理器
    console_handler=logging.StreamHandler()
    console_handler.setLevel(console_level)
​
    #创建格式化器并添加到日志处理器
    formatter=logging.Formatter(fmt=fmt)
    file_handler.setFormatter(formatter)
    console_handler.setFormatter(formatter)
​
    # 将日志处理器添加到日志器上
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)
​
    #返回日志
    return logger
​
if __name__ == '__main__':
    logger=get_logger(name='py',filename='../logs/py.log',debug=True)
    logger.info('我是普通信息')

因为直接调用common文件夹下log_handler.py中get_logger函数,会生成重复日报,故使用单例模式。

在common文件夹下新建__init__.py文件,内容如下:

import settings
from common.log_handler import get_logger
​
logger = get_logger(**settings.LOG_CONFIG)

1.2.4 测试用例的编写

登录功能:

在test_cases文件夹下新建test_login.py文件,文件中内容如下:

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
​
import settings
​
class TestLogin:
​
    def test_login_success(self):
        '''测试登录成功'''
        with webdriver.Chrome() as driver:
            # 1.访问登录页面
            driver.get(settings.PROJECT_HOST+settings.INTERFACE['login'])
            # 2.输入用户名和密码
            # 2.1 定位输入框
            # 定义元素等待时间
            wait=WebDriverWait(driver,timeout=3)
            username_input=wait.until(
                EC.visibility_of_element_located(('xpath','//input[@name="username"]'))
            )
            # 2.2 输入用户名
            username_input.send_keys(settings.TEST_NORMAL_USERNAME)
            # 2.3 定位密码框、输入密码
            wait.until(
                EC.visibility_of_element_located(('xpath', '//input[@name="password"]'))
            ).send_keys(settings.TEST_NORMAL_PASSWORD)
            # 3.点击登录
            wait.until(
                EC.visibility_of_element_located(('xpath', '//a[@name="sbtbutton"]'))
            ).click()
            # 4.断言是否登录成功
            # 断言标准怎么简单怎么来,怎么可靠怎么来,没有固定的模式,要灵活
            # 本案例中,就是判断是否出现退出按钮
            assert wait.until(EC.visibility_of_element_located(('xpath','//a[@title="退出"]')))

1.3 总结

总结当前代码优缺点:

1.代码冗余太高,每个功能测试都会写大量的重复代码。

每个用例都要打开关闭一次游览器,效率低

2.代码耦合度太高,页面稍有变动则需要修改大量的源码。

前端修改页面后,需出现修改定位信息

业务流程发生改变后,测试流程要重写

二、夹具(脚手架)的抽取和使用

因为登录功能的用例只需要打开和关闭一次游览器,故可将webdriver步骤创建在一个fixture(脚手架)中。

2.1 共享夹具的使用

2.1.1 共享夹具的定义

conftest.py为pytest的共享夹具,pytest自动发现并执行。

在test_cases文件夹中新建conftest.py模块,内容如下:

import pytest
from selenium import webdriver
​
@pytest.fixture(scope='class')
def driver():
    with webdriver.Chrome() as wd:
        # 最大化游览器
        wd.maximize_window()
        # 返回游览器对象,不能使用return,return返回之后会关闭游览器,无法进行后续操作
        yield wd

2.1.2 登录模块夹具的使用

登录模块使用共享夹具,修改test_cases文件夹下test_login.py文件,修改后内容如下:

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
​
import settings
​
class TestLogin:
​
    # 传入夹具名称driver,pytest自动发现并调用
    def test_login_success(self,driver):
        '''测试登录成功'''
        # 1.访问登录页面
        driver.get(settings.PROJECT_HOST+settings.INTERFACE['login'])
        # 2.输入用户名和密码
        # 2.1 定位输入框
        # 定义元素等待时间
        wait=WebDriverWait(driver,timeout=3)
        username_input=wait.until(
            EC.visibility_of_element_located(('xpath','//input[@name="username"]'))
        )
        # 2.2 输入用户名
        username_input.send_keys(settings.TEST_NORMAL_USERNAME)
        # 2.3 定位密码框、输入密码
        wait.until(
            EC.visibility_of_element_located(('xpath', '//input[@name="password"]'))
        ).send_keys(settings.TEST_NORMAL_PASSWORD)
        # 3.点击登录
        wait.until(
            EC.visibility_of_element_located(('xpath', '//a[@name="sbtbutton"]'))
        ).click()
        # 4.断言是否登录成功
        # 断言标准怎么简单怎么来,怎么可靠怎么来,没有固定的模式,要灵活
        # 本案例中,就是判断是否出现退出按钮
        assert wait.until(EC.visibility_of_element_located(('xpath','//a[@title="退出"]')))

2.2 混合夹具的使用

在pytest中,可以使用不同风格的夹具。当需要在一个测试类的测试执行前后输出日志时,使用pytest风格的夹具难以实现该功能,可以通过unittest或者xunit风格实现。

登录模块混合夹具的使用:

修改test_cases文件夹下test_login.py文件,将常用模块绑定到类属性,定义前后置内容,通过对象方法调用常用模块,内容如下:

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
​
import settings
from common import logger
​
class TestLogin:
    name='登录功能'
    logger=logger
    settings=settings
​
    # xunit风格的前置后置
    @classmethod
    def setup_class(cls):
        cls.logger.info('======={}测试开始======'.format(cls.name))
​
    @classmethod
    def teardown_class(cls):
        cls.logger.info('======={}测试结束======'.format(cls.name))
​
    # 传入夹具名称driver,pytest自动发现并调用
    def test_login_success(self,driver):
        '''测试登录成功'''
        # 1.访问登录页面
        driver.get(self.settings.PROJECT_HOST+self.settings.INTERFACE['login'])
        # 2.输入用户名和密码
        # 2.1 定位输入框
        # 定义元素等待时间
        wait=WebDriverWait(driver,timeout=3)
        username_input=wait.until(
            EC.visibility_of_element_located(('xpath','//input[@name="username"]'))
        )
        # 2.2 输入用户名
        username_input.send_keys(self.settings.TEST_NORMAL_USERNAME)
        # 2.3 定位密码框、输入密码
        wait.until(
            EC.visibility_of_element_located(('xpath', '//input[@name="password"]'))
        ).send_keys(self.settings.TEST_NORMAL_PASSWORD)
        # 3.点击登录
        wait.until(
            EC.visibility_of_element_located(('xpath', '//a[@name="sbtbutton"]'))
        ).click()
        # 4.断言是否登录成功
        # 断言标准怎么简单怎么来,怎么可靠怎么来,没有固定的模式,要灵活
        # 本案例中,就是判断是否出现退出按钮
        assert wait.until(EC.visibility_of_element_located(('xpath','//a[@title="退出"]')))

2.3 测试用例基类的抽取

参考接口测试思路,我们可以将公用代码抽取到基类中,并将公共模块绑定到类属性中。

2.3.1 测试用例基类的编写

测试模块前后置打印内容一致,故在test_cases文件夹下新建base_case.py文件,内容如下:

import settings
from common import logger

class BaseCase:
    '''
    测试用例基类
    '''

    # 测试套名称
    name=None
    logger=logger
    settings=settings

    # xunit风格的前置后置
    @classmethod
    def setup_class(cls):
        cls.logger.info('======={}测试开始======'.format(cls.name))

    @classmethod
    def teardown_class(cls):
        cls.logger.info('======={}测试结束======'.format(cls.name))

2.3.2 测试用例基类的使用

使测试用例继承BaseCase类实现公共模块的复用,修改test_cases文件夹下的test_login.py文件,内容如下:

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

from test_cases.base_case import BaseCase

# 继承基类
class TestLogin(BaseCase):
    name='登录功能'

    # 传入夹具名称driver,pytest自动发现并调用
    def test_login_success(self,driver):
        '''测试登录成功'''
        # 1.访问登录页面
        driver.get(self.settings.PROJECT_HOST+self.settings.INTERFACE['login'])
        # 2.输入用户名和密码
        # 2.1 定位输入框
        # 定义元素等待时间
        wait=WebDriverWait(driver,timeout=3)
        username_input=wait.until(
            EC.visibility_of_element_located(('xpath','//input[@name="username"]'))
        )
        # 2.2 输入用户名
        username_input.send_keys(self.settings.TEST_NORMAL_USERNAME)
        # 2.3 定位密码框、输入密码
        wait.until(
            EC.visibility_of_element_located(('xpath', '//input[@name="password"]'))
        ).send_keys(self.settings.TEST_NORMAL_PASSWORD)
        # 3.点击登录
        wait.until(
            EC.visibility_of_element_located(('xpath', '//a[@name="sbtbutton"]'))
        ).click()
        # 4.断言是否登录成功
        # 断言标准怎么简单怎么来,怎么可靠怎么来,没有固定的模式,要灵活
        # 本案例中,就是判断是否出现退出按钮
        assert wait.until(EC.visibility_of_element_located(('xpath','//a[@title="退出"]')))

通过运行main.py入口文件可执行用例。

至此,简单的ui自动化框架搭建完成,后续对框架进行优化。

三、po模式

3.1 po模式的定义

PO(page object)设计模式是在自动化中已经流行起来的一种易于维护和减少代码的设计模式。将每个页面单独封装为一个类,在类中定义当前页面交互的方法。在自动化测试中, PO对象作为一个与页面交互的接口。测试中需要与页面的UI进行交互时, 便调用PO的方法。这样做的好处是, 如果页面的UI发生了更改,那么测试用例本身不需要更改, 只需更改PO中的代码即可。

PO设计模式具有以下优点:

  • 测试代码与页面的定位代码(如定位器或者其他的映射)相分离。

  • 该页面提供的方法或元素在一个独立的类中, 而不是将这些方法或元素分散在整个测试中。

这允许在一个地方修改由于UI变化所带来的所有修改。

3.2 典型案例

在当前代码中,test_login.py登录测试模块就是一个不使用po模式的典型案例,存在以下问题:

  • 测试方法与定位器 (在此实例中为By.name)耦合过于严重。如果测试的用户界面更改了其定位器或登录名的输入和处理方式, 则测试本身必须进行更改。

  • 在对登录页面的所有测试中, 同一个定位器会散布在其中.

3.3 通过po模式改进

3.3.1 定义登录页面类

在根目录下新建page_objects文件夹,文件夹中新建login_page.py文件,用来定义登录页面类:

from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

import settings

class LoginPage:
    '''
    把一个页面抽象成一个类,所有这个页面上的功能封装成方法
    '''
    # 页面名称
    name='登录页面'

    # 定位信息放到类属性中
    # 用户输入框定位
    username_input_locator=('xpath', '//input[@name="username"]')
    # 密码输入框定位
    password_input_locator=('xpath', '//input[@name="password"]')
    # 登录按钮定位
    login_btn_locator=('xpath', '//a[@name="sbtbutton"]')

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

    def login(self,username,password,verify):
        '''
        登录页面的登录功能
        :param username:
        :param password:
        :param verify:
        :return:
        '''
        # 1.访问登录页面
        self.driver.get(settings.PROJECT_HOST+settings.INTERFACE['login'])
        # 2.输入用户名密码
        wait=WebDriverWait(self.driver,timeout=3)
        # 2.1 定位用户名输入框
        username_input=wait.until(EC.visibility_of_element_located(self.username_input_locator))
        # 2.2 输入用户名
        username_input.send_keys(username)
        # 2.3 定位密码输入框
        password_input=wait.until(EC.visibility_of_element_located(self.password_input_locator))
        # 2.4 输入密码
        password_input.send_keys(password)

        # 3.点击登录
        wait.until(EC.visibility_of_element_located(self.login_btn_locator)).click()

3.3.2 定义home页面类(首页)

在page_objects文件夹下创建home_page.py文件,内容如下:

from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class HomePage:
    '''
    首页
    '''
    name='首页'

    # 定位信息放到类属性中
    # logout_btn_loc=('xpath','//a[@title="退出"]')
    logout_btn_loc=('xpath','//i[@class="el-icon-switch-button"]')

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

    def get_logout_btn(self):
        '''
        获取退出按钮
        :return:
        '''
        # 返回退出按钮的元素,若无返回则报错
        try:
            return WebDriverWait(self.driver,timeout=3).until(
                EC.visibility_of_element_located(self.logout_btn_loc)
            )
        except Exception as e:
            return None

    def logout(self):
        WebDriverWait(self.driver, timeout=3).until(
            EC.visibility_of_element_located(self.logout_btn_loc)
        ).click()

3.3.3 测试逻辑优化(修改测试用例)

修改test_cases文件夹下test_login.py文件,修改完成内容如下:

from page_objects.login_page import LoginPage
from page_objects.home_page import HomePage
from test_cases.base_case import BaseCase

class TestLogin(BaseCase):
    name='登录功能'

    # 调用名为driver的夹具
    def test_login(self,driver):
        # 测试登录功能

        # 实例化登录页面
        lp=LoginPage(driver)
        # 调用登录方法
        lp.login(self.settings.TEST_NORMAL_USERNAME,self.settings.TEST_NORMAL_PASSWORD)
        # 断言
        hp=HomePage(driver)
        assert hp.get_logout_btn()

运行main.py,登录功能正常。

3.3.4 定位信息与页面对象分离

当页面比较复杂,定位信息比较多时,可以将定位信息与页面对象进行分离管理。

在项目根目录下创建page_locators文件夹,文件夹下创建login_page_locators.py模块定义login页面的定位信息为类属性,内容如下:

class LoginPageLocators:
    '''
    登录页面的定位信息
    '''

    # 定位信息放到类属性中
    # 用户输入框定位
    username_input_locator=('xpath', '//input[@name="username"]')
    # 密码输入框定位
    password_input_locator=('xpath', '//input[@name="password"]')
    # 验证码输入框定位
    verify_input_locator=('xpath', '//input[@name="verify_code"]')
    # 登录按钮定位
    login_btn_locator=('xpath', '//a[@name="sbtbutton"]')
    # 错误信息定位
    error_info_tip_loc = ('xpath', '//a[@class="layui-layer-btn0"]')

在page_locators文件夹下,新建home_page_locators.py模块,定义home页面的定位信息为类属性,内容如下:

class HomePageLocators:
    '''
    首页面定位信息
    '''

    # 定位信息放到类属性中
    logout_btn_loc = ('xpath', '//a[@title="退出"]')

然后在页面模块中导入和引用

四、页面基类的封装

4.1 封装的目的

  1. 代码复用,减少代码量,便于维护

  2. 便于调用,运行更稳定,逻辑更清晰

4.2 详细代码

可将操作功能封装到基类中进行复用,如获取一个页面元素、点击、输入等操作。

4.2.1 新建screen_shot文件夹,保存报错截屏

4.2.2 在setting.py新增,全局查找默认等待时间,错误截屏保存路径

# 全局查找默认等待时间
DEFAULT_TIMEOUT=3

# 错误截屏保存路径
ERROR_SCREENSHOT_DIR=os.path.join(BASE_DIR,'screen_shot')

4.2.3 在page_objects文件夹下新建base_page.py文件,代码内容如下:

# -*- coding: utf-8 -*-
# @Time    : 2023/3/24  14:26
# @Author  : jikkdy
# @FileName: base_page.py

import os,time
import re
from datetime import datetime

from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

import settings
from common import logger

class BasePage:
    '''
    web自动化 稳定性
    selenium - chromedriver - 浏览器 - 服务器
    页面对象的基类
    封装常用操作
    节省代码量,便于维护
    1. 查找元素 等待 - 查找
    2. 点击 等待 - 查找 - 点击
    3. 输入 等待 - 查找 - 输入
    5. 获取元素文本 等待 - 查找 -获取文本
    6. 获取元素属性 等待 - 查找 -获取属性
    7. 窗口切换
    8. 失败截图
    '''
    # 将日志、配置文件定义为类属性
    name='base页面'
    logger=logger
    settings=settings

    # 内容初始化
    def __init__(self,driver:WebDriver):
        self.driver=driver
        # 初始化元素
        self.element=None
        # 初始化定位信息
        self.locator=None
        # 初始化动作描述
        self.action=''

    def delay(self,second=0.5):
        '''
        延时操作
        :param second:秒  支持浮点数
        :return:
        '''
        time.sleep(second)
        # 定义返回对象,用于链式编程
        # 如:wait_element_is_visible(('xpath', '//input[@name="username"]'),action='输入用户名').delay(3).send_keys('11111')
        return self

    def wait_element_is_visible(self,locator,action='',**kwargs):
        '''
        等待元素可见
        :param locator:定位信息 tuple(by,expression)
        :param action: 操作说明 str
        :param kwargs: timeout(等待时间),poll_frequency(轮循时间)
        :return: page_object
        '''

        # 类属性定位信息和动作描述为None,则重新赋值,方便传递locator和action到下一个动作
        self.locator=locator
        self.action=action
        try:
            # 当有timeout参数时,使用传入的timeout值,否则使用默认的等待时长
            timeout=kwargs.get('timeout',self.settings.DEFAULT_TIMEOUT)
            # 当有poll_frequency时,使用传入参数,否是使用默认的时间间隔0.5s
            poll_frequency=kwargs.get('poll_frequency',0.5)
            # 等待元素可见并返回element信息并定义为对象属性
            self.element=WebDriverWait(self.driver,timeout,poll_frequency).until(
                EC.visibility_of_element_located(locator)
            )
        except Exception as e:
            # 记录失败日志
            self.logger.exception(
                '在{},{}操作的时候,等待{}元素可见【失败】'.format(self.name, action,locator)
            )
            # 操作失败时保存截屏
            self.get_page_screenshot(action)
            raise e
        else:
            # 操作成功记录日志
            self.logger.debug(
                '在{},{}操作的时候,等待{}元素可见【成功】'.format(self.name, action,locator)
            )
            # 返回对象,便于链式编程
            return self

    def wait_element_to_be_clickable(self,locator,action='',**kwargs):
        '''
        等待元素可被点击
        :param locator:定位信息 tuple(by,expression)
        :param action:操作说明  str
        :param kwargs:timeout(等待时间),poll_frequency(轮循时间)
        :return:
        '''
        # 类属性定位信息和动作描述为None,则重新赋值,方便传递locator和action到下一个动作
        self.locator = locator
        self.action = action
        try:
            # 当有timeout参数时,使用传入的timeout值,否则使用默认的等待时长
            timeout = kwargs.get('timeout', self.settings.DEFAULT_TIMEOUT)
            # 当有poll_frequency时,使用传入参数,否是使用默认的时间间隔0.5s
            poll_frequency = kwargs.get('poll_frequency', 0.5)
            # 等待元素可点击并返回element信息并定义为对象属性
            self.element = WebDriverWait(self.driver, timeout, poll_frequency).until(
                EC.element_to_be_clickable(locator)
            )
        except Exception as e:
            # 记录失败日志
            self.logger.exception(
                '在{},{}操作的时候,等待{}元素可点击【失败】'.format(self.name, action, locator)
            )
            # 操作失败时保存截屏
            self.get_page_screenshot(action)
            raise e
        else:
            # 操作成功记录日志
            self.logger.debug(
                '在{},{}操作的时候,等待{}元素可点击见【成功】'.format(self.name, action, locator)
            )
            # 返回对象,便于链式编程
            return self

    def wait_elment_is_loaded(self,locator,action='',**kwargs):
        '''
        等待元素加载到dom中
        :param locator:定位信息 tuple(by,expression)
        :param action:操作说明  str
        :param kwargs:timeout(等待时间),poll_frequency(轮循时间)
        :return:
        '''
        # 类属性定位信息和动作描述为None,则重新赋值,方便传递locator和action到下一个动作
        self.locator = locator
        self.action = action
        try:
            # 当有timeout参数时,使用传入的timeout值,否则使用默认的等待时长
            timeout = kwargs.get('timeout', self.settings.DEFAULT_TIMEOUT)
            # 当有poll_frequency时,使用传入参数,否是使用默认的时间间隔0.5s
            poll_frequency = kwargs.get('poll_frequency', 0.5)
            # 等待元素加载到dom并返回element信息并定义为对象属性
            self.element = WebDriverWait(self.driver, timeout, poll_frequency).until(
                EC.presence_of_element_located(locator)
            )
        except Exception as e:
            # 记录失败日志
            self.logger.exception(
                '在{},{}操作的时候,等待{}元素加载到文档【失败】'.format(self.name, action, locator)
            )
            # 操作失败时保存截屏
            self.get_page_screenshot(action)
            raise e
        else:
            # 操作成功记录日志
            self.logger.debug(
                '在{},{}操作的时候,等待{}元素加载到文档【成功】'.format(self.name, action, locator)
            )
            # 返回对象,便于链式编程
            return self

    def send_keys(self,content):
        '''
        输入字符串
        :param content: 输入的内容 str
        :return:
        '''
        # 防止在wait方法执行前调用send_keys方法
        if self.element is None:
            raise RuntimeError('不能在wait方法之前调用元素上的方法')
        try:
            # 输入之前清空输入框,防止预填
            self.element.clear()
            # 执行输入字符串操作
            self.element.send_keys(content)
        except Exception as e:
            # 定义操作失败日志
            self.logger.exception(
                '在{},{}操作的时候,对{}元素输入{}【失败】'.format(self.name, self.action, self.locator,content)
            )
            # 操作失败后截屏
            self.get_page_screenshot(self.action)
        else:
            # 操作成功后日志
            self.logger.debug(
                '在{},{}操作的时候,对{}元素输入{}【成功】'.format(self.name, self.action, self.locator, content)
            )
        finally:
            # 清空wait缓存:因执行send_keys方法后,会进行下一步其他操作,故需要清空action,locator等内容
            # 私有方法,仅在内部可调用
            self.__clear_cache()

    def get_element(self):
        '''
        获取元素
        :return:
        '''
        # 防止在wait方法执行前调用方法
        if self.element is None:
            raise RuntimeError('不能在wait方法之前调用元素上的方法')
        # 返回元素
        return self.element

    def click_element(self):
        '''
        点击元素
        :return:
        '''
        # 防止在wait方法执行前调用click方法
        if self.element is None:
            raise RuntimeError('不能在wait方法之前调用元素上的方法')
        try:
            # 执行操作
            self.element.click()
        except Exception as e:
            # 定义操作失败日志
            self.logger.exception(
                '在{},{}操作的时候,点击元素{}【失败】'.format(self.name, self.action, self.locator)
            )
            # 操作失败后截屏
            self.get_page_screenshot(self.action)
        else:
            # 操作成功后日志
            self.logger.debug(
                '在{},{}操作的时候,点击元素{}【成功】'.format(self.name, self.action, self.locator)
            )
        finally:
            # 清空wait缓存:因执行click方法后,会进行下一步其他操作,故需要清空action,locator等内容
            # 私有方法,仅在内部可调用
            self.__clear_cache()

    def click_elment_by_js(self):
        '''
        通过js点击元素
        :return:
        '''
        if self.element is None:
            raise RuntimeError('不能在wait方法之前调用元素上的方法')
        try:
            # 执行操作
            self.driver.execute_script('arguments[0].click()',self.element)
        except Exception as e:
            # 定义操作失败日志
            self.logger.exception(
                '在{},{}操作的时候,点击元素{}【失败】'.format(self.name, self.action, self.locator)
            )
            # 操作失败后截屏
            self.get_page_screenshot(self.action)
        else:
            # 操作成功后日志
            self.logger.debug(
                '在{},{}操作的时候,点击元素{}【成功】'.format(self.name, self.action, self.locator)
            )
        finally:
            # 清空wait缓存:因执行click方法后,会进行下一步其他操作,故需要清空action,locator等内容
            # 私有方法,仅在内部可调用
            self.__clear_cache()



    # def click_element(self, locator, action=''):
    #     """
    #     如果搞不懂链式,就这么写,只是效率不高
    #     """
    #     try:
    #         self.driver.find_element(*locator).click()
    #     except Exception as e:
    #         self.logger.exception(
    #             '在{},{}操作的时候,点击{}元素【失败】'.format(self.name, action,locator)
    #         )
    #         raise e
    #     else:
    #         self.logger.info(
    #             '在{},{}操作的时候,点击{}元素【成功】'.format(self.name, action,locator)
    #         )

    def get_element_text(self):
        '''
        获取元素的文本
        :return:
        '''
        # 防止在wait方法执行前调用获取元素属性方法
        if self.element is None:
            raise RuntimeError('不能在wait方法之前调用元素上的方法')
        try:
            # 执行操作
            value=self.element.text
        except Exception as e:
            # 定义操作失败日志
            self.logger.exception(
                '在{},{}操作的时候,获取{}元素的文本【失败】'.format(self.name, self.action, self.locator)
            )
            # 操作失败后截屏
            self.get_page_screenshot(self.action)
        else:
            # 操作成功后日志
            self.logger.debug(
                '在{},{}操作的时候,获取{}元素的文本【成功】'.format(self.name, self.action, self.locator)
            )
            # 返回value信息
            return value
        finally:
            self.__clear_cache()

    def get_element_attr(self,name):
        '''
        获取元素的属性
        :param name:要获取的属性名
        :return:
        '''
        # 防止在wait方法执行前调用获取元素属性方法
        if self.element is None:
            raise RuntimeError('不能在wait方法之前调用元素上的方法')
        try:
            # 执行操作
            value=self.element.get_attribute(name)
        except Exception as e:
            # 定义操作失败日志
            self.logger.exception(
                '在{},{}操作的时候,获取{}元素的{}属性【失败】'.format(self.name, self.action, self.locator, name)
            )
            # 操作失败后截屏
            self.get_page_screenshot(self.action)
        else:
            # 操作成功后日志
            self.logger.debug(
                '在{},{}操作的时候,获取{}元素的{}属性【成功】'.format(self.name, self.action, self.locator, name)
            )
            # 返回value信息
            return value
        finally:
            self.__clear_cache()

    def switch_to_new_window(self,handle=None,action=''):
        '''
        切换到新的窗口
        :param handle:窗口句柄
        :param action:
        :return:
        '''
        try:
            # 如果传入handle,则跳转到指定的窗口
            if handle:
                self.driver.switch_to.window(handle)
            else:
                # 获取当前窗口的句柄
                original_window=self.driver.current_window_handle
                # 循环当游览器的所有句柄
                for handle in self.driver.window_handles:
                    # 当句柄不等于当前句柄时,跳转句柄,进行窗口切换
                    if handle !=original_window:
                        self.driver.switch_to.window(handle)
                        break
        except Exception as e:
            self.logger.exception(
                '在{},{}操作的时候,切换到窗口{}【失败】'.format(self.name, self.action, handle)
            )
            # 报错时截屏
            self.get_page_screenshot(action)
            raise e
        else:
            self.logger.exception(
                '在{},{}操作的时候,切换到窗口{}【成功】'.format(self.name, self.action, handle)
            )

    def get_page_screenshot(self,action):
        '''
        截图功能
        获取报错时的页面截图,命名规范: 截图时间_xx页面_xx操作
        :param action:
        :return:
        '''
        img_path=os.path.join(
            self.settings.ERROR_SCREENSHOT_DIR,
            '{}_{}_{}.png'.format(
                datetime.now().strftime('%Y-%m-%d %H-%M-%S'),
                self.name,
                action
            )
        )
        self.driver.save_screenshot(img_path)
        if self.driver.save_screenshot(img_path):
            self.logger.info('生成错误截屏{}【成功】'.format(img_path))
        else:
            self.logger.info('生成错误截屏{}【失败】'.format(img_path))

    def replace_args_by_re(self,json_s,obj):
        '''
        通过正则表达式动态的替换参数
        :param json_s: 要替换的文本
        :param obj: 带有替换的文本
        :return:
        '''
        # 1.找出所有的槽位中的变量名(若公司项目中#为特殊字符,可将用例和此次#替换为其他特殊符号
        args = re.findall('#(.*?)#', json_s)
        for arg in args:
            # 2.找到obj中对应的属性,若无对应的属性,则返回None
            value = getattr(obj, arg, None)
            if value:
                json_s = json_s.replace('#{}#'.format(arg), str(value))
        return json_s


    def __clear_cache(self):
        '''
        清空wait的缓存
        :return:
        '''
        self.element=None
        self.locator=None
        self.action=''



if __name__ == '__main__':
    from selenium import webdriver
    with webdriver.Chrome() as driver:
        page=BasePage(driver)
        page.driver.get('http://testingedu.com.cn:8000/Home/user/login.html')
        # 链式编程
        page.wait_element_is_visible(('xpath', '//input[@name="username"]'),action='输入用户名').delay(3).send_keys('11111')

4.2.4 关于链式编程

class Treat():

    def eat(self):
        print('eating')
        # 返回对象
        return self

    def waring(self):
        print('warning')
        # 返回对象
        return self

    def laugh(self):
        print('laughing')
        # 返回对象
        return self

a=Treat()
# 可直接调用方法后,在后面接着调用方法
a.eat().waring().laugh()

4.2.5 修改登录页面为链式编程

修改page_objects文件夹下login_page.py文件,修改后内容如下:

from page_objects.base_page import BasePage
from page_locators.login_page_locators import LoginPageLocators as loc

class LoginPage(BasePage):
    name='登录页面'

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

    def login(self,username,password):
        '''
        登录页面的登录功能
        :param username:
        :param password:
        :param verify:
        :return:
        '''
        
        # 1.访问登录页面
        self.driver.get(settings.PROJECT_HOST+settings.INTERFACE['login'])
        # 2.输入用户名、密码和验证码
        # 链式调用,需自己封装 在方法中返回self
        # 2.1输入用户名
        self.wait_element_is_visible(
            locator=loc.username_input_locator,action='输入用户名'
        ).send_keys(self.settings.TEST_NORMAL_USERNAME)
        # 2.2 输入密码
        self.wait_element_is_visible(
            locator=loc.password_input_locator, action='输入密码'
        ).send_keys(self.settings.TEST_NORMAL_PASSWORD)

        # 3.点击登录按钮
        self.wait_element_is_visible(
            locator=loc.login_btn_locator,action='点击登录按钮'
        ).click_element()

五、登录功能测试

web自动化相对于接口自动化流程复杂,做不到绝对统一。

用例设计思路:怎么简单怎么来,怎么容易实现怎么来

在项目中登录成功和登录失败的断言方式和结果不同,故将登录成功和登录失败的测试数据分开;其中登录失败当参数错误和用户名密码错误的错误提示信息显示位置不同,进一步进行拆分。

根据测试流程的一致性,登录功能通过参数化进行。

 

5.1 用例设计

5.2 用例数据设计

用例数据保存形式的文件并没有统一,可以保存在excel、yaml、python等文件中。根据公司规定和擅长技术处理即可,在接口项目中,测试数据存放在excel中,在本项目中,直接将测试数据保存到Python文件中。

在test_data文件夹下新建login_data.py文件,文件内容如下:

# 正向用例
success_cases=[
    {
        'title':"登录成功-不记住账号密码",
        'request_data':{"username":"admin","password":"Admin@123","remember":False}
    },
    {
        'title':"登录成功-记住账号密码",
        'request_data':{"username":"admin","password":"Admin@123","remember":True}
    }
]

# 反向用例
fail_cases=[
    {
        'title':"用户名为空",
        'request_data':{"username":"","password":"Admin@123"},
        'error_tip':"请输入用户名"
    },
    {
        'title':"密码为空",
        'request_data':{"username":"","password":"Admin@123"},
        'error_tip':"请输入密码"
    },
]

# 用户名密码错误用例
error_cases=[
    {
        'title':"用户名正确密码不正确",
        'request_data':{"username":"admin","password":"Admin@12122"},
        'error_tip':"账号或密码错误"
    },
    {
        'title':"未授权的用户",
        'request_data':{"username":"xxaer","password":"Admin@123"},
        'error_tip':"此账号没有经过授权,请联系管理员"
    }
]

5.3 新增或修改测试用例编写顺序

新建或修改用例时,先修改test_cases文件夹下的测试用例模块代码,定义测试流程和方法,后在page_objects页面信息中完成对应方法的封装。

1、在test_cases文件夹中书写测试操作步骤,根据流程定义对应方法名

2、在page_objects文件夹下对应的页面.py文件中,封装测试用例中新定义的方法

5.4 登录功能测试用例

在测试用例中,增加退出后再访问登录页面,判断登录信息是否保存代码。

修改test_cases文件夹下test_login.py文件,增加日志信息,修改后,代码如下:

class TestLogin(BaseCase):
    name='登录功能'

    @pytest.mark.success
    # 数据参数化,case为数据参数名,success_cases为传入的数据
    @pytest.mark.parametrize('case',success_cases)
    def test_login_success(self,driver,case):
        '''
        登录页面的登录功能
        :param username: 用户名
        :param password: 密码
        :param remember: 是否记住账号密码
        :return:
        '''
        self.logger.info('***{}用例开始测试***'.format(case['title']))
        # 1.打开登录页面
        driver.get(self.settings.PROJECT_HOST+settings.INTERFACE['login'])
        # 2.登录
        lp=LoginPage(driver)
        lp.login(**case['request_data'])
        # 3.断言是否登录成功
        hp=HomePage(driver)
        assert hp.get_logout_btn()
        # 4.退出
        hp.logout()
        # 5.再访问登录页面
        driver.get(self.settings.PROJECT_HOST + settings.INTERFACE['login'])
        # 6.断言账号密码是否有保存
        try:
            if case['request_data']['remember']:
                # 断言登录信息是否有保存
                assert lp.get_user_value()==case['request_data']['username']
            else:
                # 断言手机号码没有保存
                assert not lp.get_user_value()
        except Exception as e:
            self.logger.exception('断言失败')
            raise e
        else:
            self.logger.info('***{}用例通过测试***'.format(case['title']))
        self.logger.info('***{}用例结束测试***'.format(case['title']))

    @pytest.mark.parametrize('case',fail_cases)
    def test_login_fail(self,case,driver):
        '''
        登录失败
        :param case: 用例数据
        :param driver: 夹具信息
        :return:
        '''
        self.logger.info('***{}用例开始测试***'.format(case['title']))
        # 1.打开登录页面
        driver.get(self.settings.PROJECT_HOST+self.settings.INTERFACE['login'])
        # 2.登录
        lp=LoginPage(driver)
        lp.login(**case['request_data'])
        # 3.断言
        try:
            assert case['error_tip']==lp.get_error_tip()
        except Exception as e:
            self.logger.exception('断言失败')
            raise e
        else:
            self.logger.info('***{}用例通过测试***'.format(case['title']))
        self.logger.info('***{}用例结束测试***'.format(case['title']))

    @pytest.mark.parametrize('case',error_cases)
    def test_login_error_username_password(self,driver,case):
        '''
        用户名或密码错误
        :param driver:
        :param case:
        :return:
        '''
        self.logger.info('***{}用例开始测试***'.format(case['title']))
        # 1.打开登录页面
        driver.get(self.settings.PROJECT_HOST + self.settings.INTERFACE['login'])
        # 2.登录
        lp = LoginPage(driver)
        lp.login(**case['request_data'])
        # 3.断言
        try:
            assert case['error_tip'] == lp.get_error_pop_tip()
        except Exception as e:
            self.logger.exception('断言失败')
            raise e
        else:
            self.logger.info('***{}用例通过测试***'.format(case['title']))
        self.logger.info('***{}用例结束测试***'.format(case['title']))

5.5 新增登录页面元素信息

5.5.1 自动消失提示元素定位

在开发者模式中进入sources页面,点击按钮,在弹出错误信息时,点击标记按钮进行暂停,后在elements中进行定位。

5.5.2 登录页面元素定位信息修改

修改page_locators文件夹下login_page_locators.py文件,增加记住登录信息定位信息,错误提示元素定位信息,修改后代码如下:

class LoginPageLocators:
    '''
    登录页面的定位信息
    '''

    # 用户信息
    username_input_locator=('xpath','//input[@placeholder="用户名"]')
    # 密码
    password_input_locator=('xpath','//input[@placeholder="密码"]')
    # 登录按钮
    login_btn_locator=('xpath','//button[@type="button"]')
    # 记住登录信息单选框
    remember_userinformation_locator=('xpath','//span[@class="el-checkbox__inner"]')
    # 错误提示框
    error_tip_locator=('xpath','//form//div[@class="form-error-info"]')
    # 错误弹框信息
    error_pop_tip_locator=('xpath','//form//div[@class="layui-layer-content"]')

5.6 修改登录页面操作

在登录功能中新增判断是否保存用户账号操作,新增获取登录页面用户名输入框的value的方法。

修改page_objects文件夹下login_page.py文件,修改后内容如下:

from page_objects.base_page import BasePage
from page_locators.login_page_locators import LoginPageLocators as loc

class LoginPage(BasePage):
    name='登录页面'

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

    def login(self,username,password,remember=True):
        '''
        登录页面的登录功能
        :param username:
        :param password:
        :param remember:
        :return:
        '''

        # 1.输入用户名
        self.wait_element_is_visible(
            locator=loc.username_input_locator,
            action='输入用户名').send_keys(username)

        # 2.输入密码
        self.wait_element_is_visible(
            locator=loc.password_input_locator,
            action='输入用户名').send_keys(password)

        # 3.勾选记住登录信息
        element = self.wait_element_is_visible(
            locator=loc.remember_userinformation_locator,
            action='勾选记住用户信息'
        ).get_element()
        # 如果remember为True执行以下操作
        if remember:
            # 如果单选框未被选中,点击单选框
            if not element.is_selected():
                element.click()
        # 当remember不为True时执行以下操作
        else:
            # 如果单选框被选中,点击取消单选
            if element.is_selected():
                element.click()

        # 4.点击登录
        self.wait_element_is_visible(
            locator=loc.login_btn_locator,
            action='点击登录').click_element()

    def get_user_value(self):
        '''
        获取用户名输入框的value
        :return:
        '''
        return self.wait_element_is_visible(
            locator=loc.username_input_locator,
            action='获取用户名输入框的值').get_element_attr('value')

    def get_error_tip(self):
        '''
        获取错误提示信息
        :return:
        '''
        return self.wait_element_is_visible(
            locator=loc.error_tip_locator,
            action='获取错误提示信息'
        ).get_element_text()

    def get_error_pop_tip(self):
        '''
        获取弹框错误提示信息
        :return:
        '''
        return self.wait_element_is_visible(
            locator=loc.error_pop_tip_locator,
            action='获取错误提示信息'
        ).get_element_text()





if __name__ == '__main__':
    from selenium import webdriver
    with webdriver.Chrome() as driver:
        page=LoginPage(driver)
        page.driver.get('http://192.168.1.185:30202/regionalSys/login')
        page.login('qqq','qqqq')
        page.delay(10)

5.7 总结

  • 在错误的用例中,因为提示信息的方式不一致,导致拆分为两个测试方法,可以当bug提出,面向用户是,提示方式要一致。

  • 第二种弹出提示信息会自动消失,定位时需要用到游览器的调试工具进行js暂停,在弹出框弹出后点击暂停按钮。

六、投资功能测试

6.1 页面操作流程

1.进入首页面

2.点击标的详情页

3.进入标的详情页

 4.输入金额

6.2 用例设计

6.3 用例数据设计

在test_data文件夹下新建invest_data.py用来存放投资用例数据,注:request_data中内容需用双引号包裹,内容如下:

# 成功用例
success_cases=[
    {
        'title':'投资100成功',
        'request_data':{"amount":100}
    },
    {
        'title':'投资200成功',
        'request_data':{"amount":200}
    },
    {
        'title':'投资500成功',
        'request_data':{"amount":500}
    },
]

# 失败用例
fail_cases=[
    {
        'title':'投资金额为0',
        'request_data':{"amount":0},
        'error_tip':"请正确填写投标金额"
    },
    {
        'title':'投资金额为负数',
        'request_data':{"amount":-100},
        'error_tip':"请正确填写投标金额"
    },
    {
        'title':'投资金额大于标的的可投金额',
        'request_data':{"amount":1000000000},
        'error_tip':"购买标的金额不能大于标剩余金额"
    }
]

6.4 定义登录成功的夹具

因投资功能测试是在登录后进行操作,故封装一个登录成功的夹具,在投资功能的用例中进行调用。

import pytest
from selenium import webdriver
from page_objects.login_page import LoginPage

@pytest.fixture(scope='class')
def driver():
    with webdriver.Chrome() as wd:
        # 最大化游览器
        wd.maximize_window()
        # 返回游览器对象,不能使用return,return返回之后会关闭游览器,无法进行后续操作
        yield wd



# 直接定义登录成功的夹具(一般不使用)
# @pytest.fixture(scope='class')
# def login_driver():
#     with webdriver.Chrome() as wd:
#         # 最大化游览器
#         wd.maximize_window()
#         # 1.登录页面
#         lp=LoginPage(wd)
#         wd.get(lp.settings.PROJECT_HOST+lp.settings.INTERFACE['login'])
#         lp.login(lp.settings.TEST_NORMAL_USERNAME,lp.settings.TEST_NORMAL_PASSWORD)
#         # 返回登录后的游览器驱动
#         yield wd


# 继承原来的夹具,增加登录功能
@pytest.fixture(scope='class')
# 参数名与上面的夹具同名
def logged_in_driver(driver):
    lp=LoginPage(driver)
    driver.get(lp.settings.PROJECT_HOST+lp.settings.INTERFACE['login'])
    lp.login(lp.settings.TEST_NORMAL_USERNAME, lp.settings.TEST_NORMAL_PASSWORD)
    # 返回webdriver
    yield driver

6.5 投资功能测试用例

在test_cases文件夹下新建test_invest.py文件,内容如下:

class TestInvest(BaseCase):
    name = '投资功能'

    @pytest.mark.parametrize('case',success_cases)
    def test_invest_success(self,logged_in_driver,case):
        self.logger.info('***{}用例开始测试***'.format(case['title']))
        # 1.进入首页面
        logged_in_driver.get(self.settings.PROJECT_HOST)
        # 2.选择第一个标
        hp=HomePage(logged_in_driver)
        hp.select_first_bid()
        # 3.获取标的投资前的可投资额
        bp=BidPage(logged_in_driver)
        bid_balance_before_invest=bp.get_bid_balance()
        # 4.获取用户投资前的余额
        user_balance_before_invest = bp.get_user_balance()
        # 5.投资
        bp.invest(**case['request_data'])
        # 6.断言是否弹出投资成功弹出框
        assert bp.get_invest_success_pop()
        # 7.刷新页面
        logged_in_driver.refresh()
        # 8.获取标的投资后的可投资额
        bid_balance_after_invest = bp.get_bid_balance()
        # 9.获取用户投资后的余额
        user_balance_after_invest = bp.get_user_balance()
        try:
            # 10.断言投资前标的可投资额-投资后标的可投资额=投资额
            assert bid_balance_before_invest - bid_balance_after_invest==case['request_data']['amount']
            # 11.断言投资前用户的余额-投资后用户的余额=投资额
            assert user_balance_before_invest - user_balance_after_invest==case['request_data']['amount']
        except Exception as e:
            self.logger.exception('断言失败')
            raise e
        else:
            self.logger.info('***{}用例通过测试***'.format(case['title']))
        self.logger.info('***{}用例结束测试***'.format(case['title']))


    @pytest.mark.parametrize('case',fail_cases)
    def test_invest_fail(self,logged_in_driver,case):
        self.logger.info('***{}用例开始测试***'.format(case['title']))
        # 1.投资
        bid=BidPage(logged_in_driver)
        bid.invest(**case['request_data'])
        # 2.断言错误信息
        try:
            assert case['error_tip']==bid.get_pop_error_tip()
        except Exception as e:
            self.logger.exception('断言失败')
            raise e
        else:
            self.logger.info('***{}用例通过测试***'.format(case['title']))
        self.logger.info('***{}用例结束测试***'.format(case['title']))

6.6 首页面点击第一个标

修改page_objects文件夹下home_page.py文件,增加第一个标定义信息,及点击第一个标的功能,修改后代码如下:

from page_objects.base_page import BasePage

class HomePage(BasePage):
    '''
    首页
    '''
    name='home页面'

    # 定位信息放到类属性中
    # logout_btn_loc=('xpath','//a[@title="退出"]')
    # logout_btn_loc=('xpath','//i[@class="el-icon-switch-button"]')
    logout_btn_loc=('xpath','//i[@class="el-icon-switch-button"]')
    # 第一个标的定位信息
    first_bid_locator=('xpath','//div[@class="b-unit"]//a')

    def get_logout_btn(self):
        '''
        获取退出按钮
        :return:
        '''
        return self.wait_element_is_visible(locator=self.logout_btn_loc,action='获取退出按钮').get_element()

    def logout(self):
        '''
        退出功能
        :return:
        '''
        self.wait_element_is_visible(locator=self.logout_btn_loc, action='获取退出按钮').click_element()

    def select_first_bid(self):
        '''
        选择第一个标
        :return:
        '''
        self.wait_element_is_visible(
            locator=self.first_bid_locator,
            action='选择第一个标'
        ).click_element()

6.7 投资页面定位及操作

在page_objects文件夹下新增bid_page.py文件,因定位信息较少,故可将元素定位信息定义为类属性,结合定位信息,封装投资页面的功能方法,其内容如下:

from page_objects.base_page import BasePage

class BidPage(BasePage):
    '''
    标的详情页
    '''
    name = '标的页面'

    # 金额输入框
    invest_input_loc=('xpath','//input[@class="form-control invest-unit-investinput"]')
    # 投资按钮
    invest_btn_loc=('xpath','//buton[text()="投标"]')
    # 错误提示信息
    error_tip_loc=('xpath','//div[@class="Text-center"]')
    # 错误提示信息确认按钮
    error_tip_btn=('xpath','//div[@class="layui-layer-btn"]/a')
    # 投资成功弹出框的按钮
    success_pop_btn_loc=('xpath','//div[contains(@id, "layui-layer")]//button[text()="查看并激活"]')

    def get_bid_balance(self):
        '''
        获取标的可投资额
        :return:
        '''
        balance=self.wait_element_is_visible(
            self.invest_input_loc,
            action='获取标的可投资额'
        ).get_element_attr('data-left')
        return float(balance)

    def get_user_balance(self):
        '''
        获取用户余额
        :return:
        '''
        balance=self.wait_element_is_visible(
            self.invest_input_loc,
            action='获取标的可投资额'
        ).get_element_attr('data-amount')
        return float(balance)

    def get_invest_success_pop(self):
        '''
        获取投资成功弹框
        :return:
        '''
        return self.wait_element_is_visible(
            self.success_pop_btn_loc,
            '获取投资成功弹窗'
        ).get_element()

    def invest(self,amount):
        '''
        投资
        :param amount:
        :return:
        '''
        # 1.输入金额
        self.wait_element_is_visible(self.invest_input_loc,'投资').send_keys(amount)
        # 2.点击投资按钮
        self.wait_element_is_visible(self.invest_input_loc, '投资').click_element()

    def get_pop_error_tip(self):
        '''
        获取弹出的错误提示信息
        :return:
        '''
        text=self.wait_element_is_visible(
            self.error_tip_loc,
            '获取错误提示信息'
        ).get_element_text()
        # 关闭错误提示弹窗
        self.wait_element_is_visible(
            self.error_tip_btn,
            '点击错误信息确认按钮'
        ).click_element()
        return text

6.8 总结

  • 优先主流程,时间允许就全面覆盖

  • 部分前置可提前准备放到设置中提高效率

  • 配合问题,比如标的的设计,测试时环境是否独立,如果团队测试需要声明配合

  • 数据库校验(根据实际需要,同接口测试)

七、自定义执行内容

项目实操中,我们只需要执行部分用例,而又不想改变测试运行的主逻辑。这时就可以使用 pytest 的标记运行来过滤。

7.1 注册标记

在项目根目录下创建pytest.ini文件用来定义注册标记,内容如下:

[pytest]
markers =
    success: marks success tests
    fail
    login
    invest
    first

定义了success、fail、login、invest四个标记,冒号后面是标记的注释内容,可省略。

7.2 标记测试

7.2.1 标记方法

在要标记的测试方法上应用装饰器

@pytest.mark.mark_name
def some_test_function():
	pass

运行:

在main.py入口文件中,增加-m参数

修改后代码如下

7.2.2 标记测试类

与标记方法一致,在要标记的类定义语句上方应用装饰器

@pytest.mark.mark_name
class TestClass():
	pass

运行方式与运行标记的测试方法一致,通过-m 指定运行的标记名。

7.2.3 标记测试模块

标记模块需要在模块顶部创建全局变量 pytestmark

import pytest
pytestmark = pytest.mark.mark_name
# 或者多个标记
# pytestmark = [pytest.mark.mark_name1, pytest.mark.mark_name2]

7.2.4 分开标记参数化的用例

将指定数据定义为标记

写法一:

import pytest
success_cases[0]=pytest.param(success_cases[0],marks=pytest.mark.first)

写法二:

import pytest
# 正向用例
success_cases=[
    pytest.param({
        'title':"登录成功-不记住账号密码",
        'request_data':{"username":"admin","password":"Admin@123","remember":False}
    },marks=pytest.mark.first),
    {
        'title':"登录成功-记住账号密码",
        'request_data':{"username":"admin","password":"Admin@123","remember":True}
    }
]

7.3 运行指定标记

当使用自定义的标记对测试做好标记后,在pytest命令中使用 -m 参数即可选择指定的标记运行。

#命令行模式
pytest -v -m success
# 多个标记可以用 and or进行组合
pytest -v -m "mark1 and mark2" # 选取同时被mark1和mark2标记的测试
pytest -v -m "mark1 or mark2" # 选取被mark1或mark2标记的测试


#主函数模式
if __name__ == '__main__':
    pytest.main(['-vs','-m=smoke or product'])

7.4 通过命令行动态指定标记

在实际运行项目时,需要根据测试任务的不同,需要在命令行使用 -m 参数动态执行对应的用例,所以我们需要能够动态的接收命令行参数。

7.4.1 获取传入的命令行参数

在common文件夹下新建tools.py文件,来获取传入的命令行参数,代码如下:

import sys


def get_opts(name):
    '''
    获取传入的命令行参数
    :param name:
    :return:
    '''
    args=sys.argv[1:]
    # 如果命令行中参数名有多个值,则需要使用""进行包裹
    # 如 -m "success and login"
    if name in args:
        return args[args.index(name)+1]


if __name__ == '__main__':
    # 可返回命令行的参数
    res=sys.argv
    print(res)

7.4.2 定义入口函数

修改入口函数处代码,让其动态获得参数信息,并执行代码,修改后内容如下:

import pytest
import settings
from common.tools import get_opts

if __name__ == '__main__':
    # pytest.main(['-s','-v','-m success','--alluredir=./reports', settings.TEST_CASE_DIR])
    # 定义传入的参数信息
    args=['-s','-v','--alluredir=./reports', settings.TEST_CASE_DIR]
    # 获取命令行-m参数的值
    arg=get_opts('-m')
    # 若-m参数不为空,则将参数和参数值传入参数信息中
    if arg:
        args.insert(0,'-m {}'.format(arg))
    # 运行代码并生成测试报告
    pytest.main(args)

7.4.3 部分报错调整

当指定执行fail标记的用例时,可能存在报错部分,如投资用例。

八、通过自定义参数指定不同游览器测试

在项目中,通常需要对不同游览器进行兼容性测试,故在对代码进行解耦,使其可以根据命令行指定执行游览器。

8.1 添加自定义参数

因为pytest中无browser参数,故需要添加自定义参数。

pytest框架要添加自定义参数需要在 conftest.py 模块中(也只能在此模块中)实现钩子函数pytest_addoption 。

修改test_cases文件夹下conftest.py文件,新增如下内容:

def pytest_addoption(parser):
    # 定义pytest的参数
    parser.addoption("--browser",default='chrome')   # 坑 参数都是--小写

官方网址

8.2 设置游览器驱动路径

在根目录下新建drivers文件夹,用来存放游览器驱动(建议附上驱动对应版本信息)

在配置文件setting.py中添加游览器驱动位置:

# 浏览器驱动
BROWSER_DRIVERS = {
    'chrome': os.path.join(BASE_DIR, 'drivers', 'chromedriver_90.exe'),
    'edge': os.path.join(BASE_DIR, 'drivers', 'msedgedriver_90.exe')
}

8.3 在夹具中接收参数

pytest框架在执行用例前会把所有的参数封装到一个对象中,使用内置夹具 pytestconfig 可以获取对应的参数。

所以,自定义夹具只需要使用 pytestconfig 即可。

我们通过自定义 –browser 参数来指定运行测试的浏览器,定义打开浏览器的夹具,重写driver方法,在test_cases文件夹下conftest.py文件新增内容, 如下:

@pytest.fixture(scope='class')
def driver(pytestconfig):
    if pytestconfig.getoption('--browser')=='edge':
        with webdriver.Edge(executable_path=settings.BROWSER_DRIVERS['edge']) as wd:
            wd.maximize_window()
            yield wd
    elif pytestconfig.getoption('--browser')=='chrome':
        with webdriver.Edge(executable_path=settings.BROWSER_DRIVERS['chrome']) as wd:
            wd.maximize_window()
            yield wd

8.4 修改main.py函数

修改main.py代码可接受命令行–browser参数,修改后代码如下:

import pytest
import settings
from common.tools import get_opts

if __name__ == '__main__':
    # pytest.main(['-s','-v','-m success','--alluredir=./reports', settings.TEST_CASE_DIR])
    # 定义传入的参数信息
    args=['-s','-v','--alluredir=./reports', settings.TEST_CASE_DIR]
    # 获取命令行-m参数的值
    arg=get_opts('-m')
    # 若-m参数不为空,则将参数和参数值传入参数信息中
    if arg:
        args.insert(0,'-m {}'.format(arg))
    arg = get_opts('--browser')
    if arg:
        args.insert(0,'--browser=={}'.format(arg))   # 坑:这里只能写key=value的形式,不要用空格
    # 运行代码并生成测试报告
    pytest.main(args)

8.5 运行

在命令行中指定标记和游览器运行

 python main.py -m "success and login" --browser edge

九、定位及部分补充内容

9.1 考勤功能扩展(类属性的传递)

9.1.1 考勤功能操作流程

9.1.2 测试用例编写

讲考勤数字、考勤人数、考勤标题等定义为类属性,方便传递和调用。

在test_cases文件夹下新建文件test_attend.py文件,内容如下:

# -*- coding: utf-8 -*-
# @Time    : 2023/4/13  11:51
# @Author  : jikkdy
# @FileName: test_attend.py
import pytest
from test_cases.base_case import BaseCase

from page_objects.login_page import LoginPage
from page_objects.main_page import MainPage
from page_objects.course_page import CoursePage
from test_data.attend_data import student_list

class TestAttendFlow(BaseCase):
    name = '考勤功能业务流'

    def test_create_number_attendance(self,driver):
        '''
        老师创建数字考勤
        :param driver:
        :return:
        '''
        self.logger.info('--------开始测试老师创建数字考勤----------')
        try:
            # 1.访问登录页面
            driver.get(self.settings.PROJECT_HOST+self.settings.INTERFACE['login'])
            # 2.登录老师账号
            lp=LoginPage(driver)
            lp.login(self.settings.TEST_TEC_USERNAME,self.settings.TEST_TEC_PASSWORD)
            # 3.进入对应课程
            mp=MainPage(driver)
            mp.select_first_teaching_course()
            # 4.点击考勤标签
            # 5.点击新建考勤
            # 6.选择数字考勤
            # 7.点击确定
            # 8.设置考勤时长并确定
            cp=CoursePage(driver)
            title,attend_code=cp.create_number_attendance()

            # 绑定到类属性中共享
            self.__class__.title=title
            self.__class__.attend_code=attend_code
        except Exception as e:
            raise AssertionError('****创建数字考勤失败*****')
        self.logger.info('----创建数字考勤成功,title={},考勤码={}----'.format(title,attend_code))

    @pytest.mark.parametrize('case',student_list)
    def test_student_attend(self,driver,case):
        '''
        学生登录并签到
        :param driver:
        :param case:
        :return:
        '''
        self.logger.info('------开始测试学生签到数字考勤-------')
        # 1.访问登录页面
        driver.get(self.settings.PROJECT_HOST+self.settings.INTERFACE['login'])
        # 2.登录学生账号
        lp=LoginPage(driver)
        lp.login(**case)
        # 3.进入课程
        mp=MainPage(driver)
        mp.select_first_learning_course()
        # 4.签到
        cp=CoursePage(driver)
        cp.stu_number_attend(self.attend_code)
        cp.delay(3)
        tm,title,res=cp.get_first_attendance_record()
        assert (title,res)==(self.title,'出勤')

        # 绑定考勤人数可以做结束考勤时的检验
        # 获取对象的attend_num属性,若无该属性给attend_num赋值为0
        attend_num=getattr(self,'attend_num',0)
        # 每执行一个用例,attend_num加1
        self.__class__.attend_num=attend_num+1
        self.logger.info('-----签到成功,共签到{}位学生----'.format(attend_num+1))

    def test_close_attendance(self,driver):
        '''
        结束考勤
        :param driver:
        :return:
        '''
        self.logger.info('-----开始测试结束考勤--------')
        # 1.访问登录页面
        driver.get(self.settings.PROJECT_HOST+self.settings['login'])
        # 2.登录老师账号
        lp=LoginPage(driver)
        lp.login(self.settings.TEST_TEC_PASSWORD,self.settings.TEST_TEC_PASSWORD)
        # 3.进入课程
        mp=MainPage(driver)
        mp.select_first_teaching_course()
        # 4.结束考勤
        cp=CoursePage(driver)
        # 有两个返回值,第二个返回值在后续中不使用,故使用_接收
        num,_=cp.close_number_attendance()

        assert num==self.attend_num
        self.logger.info('-----结束考勤测试,共签到{}学生--------'.format(num))

9.2 定位操作补充

查看定位的元素是否有事件(事件监听),若无事件无法进行点击等操作

1.定位元素中有数字时,尽量采取其他信息定位,因为数字可能是动态生成,每次内容不一致

2.span标签一般不绑定事件,故需要定位到其父元素

//span[text()="尹文杰测试商家"]/parent::li   # 定位元素的父元素且标签名为li
//span[text()="尹文杰测试商家"]/.. 		  # 定位元素的父元素

3.存在部分元素,在事件监听内容中有click,但是代码进行点击操作报错,可能是因为前端更新过快,游览器没跟上,导致游览器驱动不支持该事件,故使用js进行点击

在开发者工具中console部分,通过$x(‘定位信息’)[0].click()可以点击到

 在selenium框架中执行js代码

 因基类中封装了执行js代码,在代码中调用click_elment_by_js方法进行点击

4.当页面中有同样的span文本信息时,可用过父元素属性再定位到元素

//div[@aria-label="考勤"]//span[text()="确定"]/parent::button
# //div[@aria-label="考勤"]    根目录下div标签中aria-label值为考勤的元素
# //span[text()="确定"]    指定的div下目录,span元素文本为确定的元素
#/parent::button			父标签名为button

5.存在在页面可以看到内容但在元素定位信息中无内容,可通过value取得相应的值

self.element.get_element_attr('value')

6.当元素无法直接定位到时,可通过定位其附近的元素对其进行定义

//div[@class="sign-in"]/parent::div/preceding-sibling::div/span
#//div[@class="sign-in"]    class为sign-in的div元素
#/parent::div				标签名为div的父元素
#/preceding-sibling::div/span		上一个标签名为div的兄弟元素下的span标签


//div[@class="sign-in"]/parent::div/following-sibling::div//span[text()="结束"]/parent::button
#//div[@class="sign-in"]    class为sign-in的div元素
#/parent::div				标签名为div的父元素
#/following-sibling::div//span[text()="结束"]		下一个标签名为div的兄弟元素下的span文本信息为结束的span标签
#/parent::button			父标签名为button的元素

9.3 allure的html报告生成

在reports文件夹下新建temp文件夹用来存放生成的json文件夹,在reports文件夹下新建report文件夹用来存放生成的html报告,使用os.system生成html报告

修改main文件为如下内容:

import pytest
import os
import settings
from common.tools import get_opts

if __name__ == '__main__':
    # pytest.main(['-s','-v',"--alluredir=./reports/temp", settings.TEST_CASE_DIR])
    # os.system('allure generate ./reports/temp -o ./reports/report --clean')
    # 定义传入的参数信息
    args=['-s','-v','--alluredir=./reports/temp', settings.TEST_CASE_DIR]
    # 获取命令行-m参数的值
    arg=get_opts('-m')
    # 若-m参数不为空,则将参数和参数值传入参数信息中
    if arg:
        args.insert(0,'-m {}'.format(arg))
    arg = get_opts('--browser')
    if arg:
        args.insert(0,'--browser=={}'.format(arg))   # 坑:这里只能写key=value的形式,不要用空格
    # 运行代码并生成测试报告
    pytest.main(args)
    os.system('allure generate ./reports/temp -o ./reports/report --clean')

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
扎眼的阳光的头像扎眼的阳光普通用户
上一篇 2023年8月8日
下一篇 2023年8月8日

相关推荐