一次关于webUI自动化测试的练习

一、准备

  • IDEA工具:pycharm(社区版即可)
  • Python3.9
  • Webdriver.exe文件
  • Chrome浏览器

注意:

  • 需要将Webdriver.exe文件放到本地python的lib文件夹下,或者在代码中指定驱动的路径,如:driver = webdriver.Chrome(executable_path='driver/chromedriver.exe')
  • Webdriver.exe的版本需要和浏览器版本一致,不一致则会报错,浏览器版本可通过浏览器 “设置” --> “关于Chrome” 查看(Webdriver.exe下载地址

二、初体验

1、实现用户登录

from time import sleep
from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get(r'https://xxx')      # 打开浏览器并访问该链接,这里的链接不便展示哈
driver.maximize_window()

# 定位元素并操作
driver.find_element(By.NAME, 'username').send_keys('luoyang')
driver.find_element(By.NAME, 'password').send_keys('123456')
driver.find_element(By.XPATH, '//*[@id="app"]/div/div[2]/div/form/button').click()
sleep(10)

# 关闭并退出浏览器
driver.quit()
"""关于close()和quit():close()只是关闭浏览器当前窗口,并不会退出浏览器
当浏览器只有一个窗口时,使用close()虽然退出了浏览器,但驱动还在运行
而quit()则会关闭所有窗口,清除session,并结束驱动运行
"""

2、引入unittest框架

from time import sleep
from selenium import webdriver
import unittest
from selenium.webdriver.common.by import By


class Login(unittest.TestCase):

    def setUp(self) -> None:
        self.driver = webdriver.Chrome()
         self.url = r'https://xxx'
         self.driver.maximize_window()  # 最大化窗口
         self.driver.get(self.url)
 
    def test_login(self, username='luoyang', password='123456'):
         self.driver.find_element(By.NAME, 'username').send_keys(username)
         self.driver.find_element(By.NAME, 'password').send_keys(password)
         self.driver.find_element(By.XPATH, '//*[@id="app"]/div/div[2]/div/form/button').click()
 
    def tearDown(self) -> None:
        sleep(5)
         self.driver.quit()
 

if __name__ == '__main__':
     unittest.main()     # 执行测试

三、POM设计模式

即page object model,页面对象模型,顾名思义,就是将每个页面当做一个对象来看待,将页面中需要操作的元素提取到这个对象中,此后每当要用到这些元素时,调用该对象即可。让我们来具体使用一下吧!

    首先,我们先创建好结构:
      all_case_run.py --模块,用于执行所有的测试类,并生成测试报告
      |--common -- 包,用于存放公用的工具模块
         |--util.py -- 通用工具模块
      |--case -- 包,用于存放所有的测试类
        |--test_login.py -- 登录测试用例模块
      |--pages -- 包,用于存放页面类及页面基类(basePage)
        |--base_page.py -- 所有页面对象都需继承该模块的BasePage类,该类里封装了元素的定位、操作等方法
        |--login_page.py -- 登录页面模块,该模块包含了登录页面的元素、元素定位及操作逻辑等
      |--data -- 包,用于存放元素定位路径文件
        |--login.yaml -- yaml数据文件,用于存放登录页面的元素定位路径数据
      |--report -- 包,用于存放测试报告文件及日志文件

      至此,一个简便的结构就创建好了。

all_case_run.py

import time
from BeautifulReport import BeautifulReport
from GAD_test.common.util import get_path
from GAD_webUI.commen.send_email import SendEmail


def createSuite(case_dir=os.path.join(get_path(), 'case')):
    """
     将 discover() 方法筛选出来的用例,循环添加到 suite 中
    """
    # 创建测试套件容器
    test_suite = unittest.TestSuite()
    # 找到指定目录下的所有测试模块
    discover = unittest.defaultTestLoader.discover(case_dir)

    # 将 discover 中的测试用例循环添加到 suite 中
    for testCases in discover:
        for testCase in testCases:
            test_suite.addTest(testCase)

    return test_suite


if __name__ == '__main__':
    now = time.strftime('%Y-%m-%d_%H_%M_%S', time.localtime(time.time()))
    filename = 'D:\\testStudy\gitstudy\gitrepository\pythonstudy\pythonworkspace\GAD_webUI\\report'
    result = BeautifulReport(createSuite())     # 运行测试并生成测试结果
    result.report(filename=now+'GAD_smoke', description='GAD冒烟测试', report_dir=filename)     # 生成测试报告,这里采用的是 BeautifulReport

util.py

import os
import yaml
import pandas as pd
import configparser


def get_path():
    cwd_path = os.path.dirname(__file__)
    root_path = os.path.split(cwd_path)[0]  # 获取当前项目根目录

    return root_path


def read_yaml(file_path):
    with open(file_path, 'r', encoding='utf-8') as f:
        data = yaml.load(f, Loader=yaml.CLoader)
    return data  # 取值:value = data['section]['key]


def read_config(file_path):
    config = configparser.ConfigParser()
    config.read(file_path, encoding='utf-8')
    return config  # 取值:value = config['section]['key]


def read_excel(file_path):
    # 读取Excel文件并将DataFrame对象转化为列表对象
    data = pd.read_excel(file_path, sheet_name='Sheet1').values.tolist()
    return data


basePage.py

"""
所有页面类都需继承该类,该类封装了Selenium 基本方法(元素定位、元素等待、等)
"""
from selenium.common.exceptions import NoSuchAttributeException, NoSuchElementException, TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from common.record_log import logger


class BasePage(object):

    def __init__(self, driver):
        self.driver = driver
        self.driver.implicitly_wait(20)  # 隐式等待,设置一次全局有效
        self.driver.maximize_window()  # 最大化窗口

    def find_element(self, element_xp: str):
        """
            单元素定位
        :param element_xp: 该元素的xpath路径
        :return: WebElement 对象
        """
        try:
            return WebDriverWait(self.driver, 10, 0.5).until(
                EC.presence_of_element_located((By.XPATH, element_xp)))
        except NoSuchElementException:
            logger.error('未找到元素:{}'.format(element_xp))
        except TimeoutException:
            logger.error('元素:{} 定位超时'.format(element_xp))

    def find_elements(self, element_xps: str):
        """
            多元素定位
        :param element_xps: 此类元素的xpath路径
        :return: WebElement 对象集
        """
        try:
            return WebDriverWait(self.driver, 10, 0.5).until(
                EC.presence_of_all_elements_located((By.XPATH, element_xps)))
        except NoSuchElementException:
            logger.error('未找到元素:{}'.format(element_xps))
        except TimeoutException:
            logger.error('元素:{} 定位超时'.format(element_xps))

    # 点击元素,以JS脚本的方式
    def click_JS(self, element_xp: str):
        element = self.find_element(element_xp)
        self.driver.execute_script('arguments[0].click();', element)

    # 点击元素
    def click(self, element_xp: str):
        try:
            self.find_element(element_xp).click()
        except NoSuchAttributeException:
            logger.error('元素{}属性不可用'.format(element_xp))

    # 输入框输入值
    def send_kw(self, element_xp: str, kw: str):
        element = self.find_element(element_xp)
        element.send_keys(kw)

    # 清除输入框
    def clear(self, element_xp: str):
        self.find_element(element_xp).clear()

    # 鼠标移动到指定元素上
    def move_element(self, element_xp: str):
        element = self.find_element(element_xp)
        ActionChains(self.driver).move_to_element(element).perform()

    # 双击元素
    def double_click(self, element_xp: str):
        element = self.find_element(element_xp)
        ActionChains(self.driver).double_click(element).perform()

    # 切换到指定窗口
    def switch_window(self, num: int):
        handles = self.driver.window_handles  # 获取当前窗口句柄集合
        self.driver.switch_to.window(handles[num])  # 切换到指定窗口

    # 刷新页面
    def refresh_page(self):
        self.driver.refresh()

loginPage.py

from GAD_webUI.commen.util import get_yaml
from GAD_webUI.pages.base_page import BasePage


class LoginPage(BasePage):

    login_els = get_yaml(
        r'D:\GAD_webUI\data\login.yaml')  # login_els是个字典

    def login_GAD(self, username, password):

        self.open_page()  # 打开浏览器
        self.send_kw(self.login_els['username'], username)  # 输入用户名
        self.send_kw(self.login_els['password'], password)  # 输入密码
        self.click(self.login_els['login_btn'])  # 点击登录

		# 获取登录失败时的弹窗元素
		error_el = self.find_element_p((By.XPATH, self.login_els['login_error']))
        if error_el:
            return error_el.text
        else:
            print('登录成功')

test_login.py

import unittest
from time import sleep
from selenium import webdriver
from GAD_webUI.pages.login_page import LoginPage


class Login(unittest.TestCase):
    driver = webdriver.Chrome()

    @classmethod
    def setUpClass(cls, ) -> None:
        cls.login_page = LoginPage(cls.driver)

    def test_login(self, username='v-luoyang', password='123456'):
        error_text = self.login_page.login_GAD(username, password)
        self.assertFalse(error_text is not None, msg=error_text)  # 如果错误信息存在,则登录失败,输出错误提示信息

    @classmethod
    def tearDownClass(cls) -> None:
        sleep(5)
        cls.driver.quit()


if __name__ == '__main__':  # 执行all_test_run.py 时,需将该段注释掉
    unittest.main()

如果还需发送邮件,则可使用以下代码 send_email.py

import os
from email.mime.application import MIMEApplication

"""
这个文件主要是配置发送邮件的主题、正文等,将测试报告发送并抄送到相关人邮箱的逻辑。
"""
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from email.utils import parseaddr, formataddr


class SendEmail(object):
    def __init__(self, username, passwd, recv, title, content,
                 file_path=None, ssl=False,
                 email_host='smtp.163.com', port=25, ssl_port=465):
        self.username = username  # 发送邮箱用户名
        self.passwd = passwd  # 发送邮箱授权密码
        self.recv = self._format_addr(recv)  # 收件人,多个要传list ['a@qq.com','b@qq.com]
        self.title = title  # 邮件标题
        self.content = content  # 邮件正文
        self.file_path = file_path  # 附件路径,如果不在当前目录下,要写绝对路径
        self.email_host = email_host  # smtp服务器地址
        self.port = port  # 普通端口
        self.ssl = ssl  # 是否安全链接
        self.ssl_port = ssl_port  # 安全链接端口
        self.smtp = smtplib.SMTP(self.email_host, port=self.port) if self.ssl else smtplib.SMTP_SSL(
            self.email_host, port=self.ssl_port)

    @staticmethod
    def _format_addr(s):
        """
        格式化邮件地址,防止中文编码错误
        :param s: 邮件地址 str
        :return:
        """
        name, addr = parseaddr(s)
        return formataddr((Header(name, 'utf-8').encode(), addr))

    @staticmethod
    def _att_html(html_file_path):
        """
         构造邮件html附件
        :param html_file_path: 附件所在完整路径
        :return: att_html(MIMEApplication object)
        """
        with open(html_file_path, 'rb') as file:
            att_html = MIMEApplication(file.read())
            att_html.add_header('Content-Disposition', 'attachment', filename=os.path.basename(html_file_path))

        return att_html

    # 发送邮件
    def send_email(self):
        msg = MIMEMultipart()
        msg.attach(MIMEText(self.content, _charset='utf-8'))  # 邮件正文的内容
        msg.attach(self._att_html(self.file_path))  # 构造附件

        msg['Subject'] = self.title  # 邮件主题
        msg['From'] = self.username  # 发送者账号
        msg['To'] = ','.join(self.recv)  # 接收者账号列表

        self.smtp.set_debuglevel(1)  # 打印出和SMTP服务器交互的所有信息
        # 登录发送邮件服务器
        self.smtp.login(self.username, self.passwd)
        try:
            self.smtp.sendmail(self.username, self.recv, msg.as_string())
        except Exception as e:
            print('出错了。。', e)
        else:
            print('发送成功!')
        self.smtp.quit()


if __name__ == '__main__':
	# 目前仅尝试了163邮箱发送邮件到QQ邮箱中,可以发送成功,其他未尝试
    m = SendEmail(
        username='这里输入自己的邮箱,如 cicadaxxx@163.com',
        passwd='这里输入自己163邮箱的授权密码,如 OEGMVWFSIYOWQKBD(这里的授权密码我编造的不可以用哦)',
        recv=['这里是接收方的邮箱'],	# 可以填多个,多个邮箱地址间要用英文逗号隔开,如['1761xxx@qq.com', '319xxx@qq.com']
        title='冒烟测试报告',	# 邮件标题
        content='您好请下载查看附件',	# 邮件正文
        file_path='cicadaLuo.htm',	# 附件文件路径
        ssl=True,
    )
    m.send_email()

引入DDT(data driver test 数据驱动测试)

  • ① 安装DDT,打开cmd,输入pip install ddt
  • ② 在测试类上写上@ddt,表示该用例类需要进行数据驱动
  • ③ 在测试方法上写上@file_data(file_path),表示引入外部文件进行数据驱动。
  • ④ 如果步骤③传入的文件是yaml格式,那么用例方法参数需要用**args来接收文件的内容(表示接收文件的所有内容到该参数中) ;如果传入的文件是其他的格式,那么用一个参数接收即可(接收的是json数据格式的值)
import unittest
from ddt import file_data, ddt
from selenium import webdriver
from GAD_webUI.pages.login_page import LoginPage


@ddt
class test_Login(unittest.TestCase):
    def setUp(self) -> None:
        self.driver = webdriver.Chrome()
        self.login_page = LoginPage(self.driver)

    def tearDown(self) -> None:
        self.driver.quit()

    @file_data(r'D:\G_webUI\data\user_login.yaml')
    def test_login(self, **kwargs):
        print(kwargs)
        error_text = self.login_page.login_GAD(kwargs['username'], kwargs['password'])
        self.assertFalse(error_text is not None, msg=error_text)  # 如果错误信息存在,则登录失败,输出错误提示信息


if __name__ == '__main__':  # 执行all_test_run.py 时,需将该段注释掉
    unittest.main()

四、问题及解决思路

1、元素定位不到怎么办?

表现形式为:程序抛出 NoSuchElementException 异常

解决思路:

  • 检查元素定位属性值是否写错,很多时候错误都是因为粗心导致的
  • 添加等待。有时,程序执行过快,导致程序已经执行完了,而元素还未加载出来,那么获取不存在的元素自然就会报错了,最粗暴的做法就是 sleep(3)—强制等待3秒,这样做使得程序运行时间较长,一般少用。最常用的是使用显示等待(搭配 until()方法、expected_conditions 类来使用)。
    例:
from selenium.common.exceptions import NoSuchElementException, TimeoutException
from selenium.webdriver.support import expected_conditions as EC

# 单个元素的定位方法3
def find_element_p(self, *args):	# args, 即(By.XPATH, element_xps)(定位方式,元素路径)
    try:
        return WebDriverWait(self.driver, 5, 0.5).until(EC.presence_of_element_located(*args))
    except (NoSuchElementException, TimeoutException):
         print("超过元素定位等待时长,无法获取到该元素,请检查定位路径")

  • 以上方法不行时,那么就再尝试使用其他方式进行元素定位(常见的元素定位方式可是有八种之多)

2、元素无法交互

表现形式为:程序抛出异常:ElementNotInteractableException: Message: element not interactable

解决思路:

  • 检查进行交互的元素是否唯一,元素不唯一时也会出现此类错误
  • 检查元素是否被隐藏。如果元素被隐藏起来,也无法进行交互。常见案例有:某按钮需要鼠标悬停在该元素上才能进行交互操作,此时就需要用到 ActionChains 类。例:
# 鼠标移动到指定元素上
def move_element(self, element_xp):
    move = self.find_element_p((By.XPATH, element_xp))
    ActionChains(self.driver).move_to_element(move).perform()

五、拓展

1、装饰器 skip、skipif、skipUnless、expectedFailure 的使用

import unittest
from selenium import webdriver


class Test_exercise(unittest.TestCase):
    def setUp(self) -> None:
        self.driver = webdriver.Chrome()

    def tearDown(self) -> None:
        self.driver.quit()

    @unittest.skip(reason='')  # 表示跳过该测试用例,reason:跳过原因
    def test_1(self):  # 测试用例
        self.assertTrue(1 + 1 > 2)

    @unittest.skipIf(condition='布尔表达式', reason='')  # 表达式为 true 跳过该测试用例
    def test_2(self):  # 测试用例
        self.assertTrue(10 % 2 == 0)

    @unittest.skipUnless(condition='布尔表达式', reason='')  # 表达式为 true 则执行该测试用例
    def test_3(self):  # 测试用例
        i = -2
        self.assertTrue(abs(i) == -i)

    @unittest.expectedFailure  # 预期失败,即该用例执行失败时不会算作失败
    def test_4(self):  # 测试用例
        i = -2
        self.assertTrue(abs(i) == i)


if __name__ == '__main__':
    unittest.main()

2、断言(一种检查实际结果与期望结果关系的方式)

常见的几种断言方式如下:

  • assertEqual(a, b)    a=b 则返回 True
  • assertNotEqual(a, b)    a=b 则返回 False
  • assertTrue(exp)    表达式为True 则返回 True
  • assertFalse(exp)    表达式为True 则返回 False
  • assertIs(a, b)    a is b 则返回 True
  • assertIsNot(a, b)    a is b 则返回 Fasle

3、测试用例执行顺序

  1. 默认按照 ASCII 码值排序
  2. 重写 TestLoader 类中的排序方法并自定义发现用例的规则(这个可以忽略,用默认的用例执行顺序就好了)

4、unittest 组件介绍

1. TestCase

一个完整的测试单元,执行该测试单元可以完成对某一个问题的验证,是所有用例类的父类,用例类需继承它才可被 unittest 发现并执行

2. TestSuite

测试套件,看作是多个用例的集合(容器),例:
def create_suite():
    suite = unittest.TestSuite()    # 创建suite
    suite.addTest(TestA("test_1"))  # 往suite里添加用例
    suite.addTest(TestA("test_2"))  # 往suite里添加用例
    return suite    # 返回添加完用例的suite

3. TestLoader

用来寻找 test case 并将其加载到 test suite 中,提供了以下几种方法寻找(发现)test case,如下:
  • unittest.TestLoader().loadTestsFromTestCase(testCaseClass)
          testCaseClass: 必须是 TestCase 的子类或孙类
          

  • unittest.TestLoader().loadTestsFromModule(module, pattern)
          model:TestCase(用例)所在模块
          pattern:str 类型,发现用例的规则,默认发现 test 开头的用例

  • unittest.TestLoader().loadTestsFromName(name)
          name:str 类型,格式要求为 "model.class.method"

  • unittest.TestLoader().loadTestsFromNames(names)
          names:list 类型, 格式要求同上

  • unittest.TestLoader().discover(path_dir, pattern, top_level_dir)
          path_dir:str 类型,TestCase 文件路径
          pattern:同上
          top_level_dir:str 类型,TestCase 的顶层目录,默认为 None

实例详见:all_case_run.py

4. TestRunner

测试执行器,执行 suite 中的用例,并将结果保存到 TextTestResult 实例中,例:
if __name__ == '__main__':  # 执行当前类的所有测试用例
    suite = unittest.TestSuite()
    cases = unittest.TestLoader().loadTestsFromName("unittest_exercise.unit_exercise.TestA")
    for case in cases:
        suite.addTest(case)

    runner = unittest.TextTestRunner()  # 创建 runner 实例
    runner.run(suite)  # 执行 suite 中的用例

5. TestFixture

用于测试用例执行前后的工作,最常用的是 setUp()、tearDown()方法,例:
import unittest


def compare(a, b):  # 待测试方法
    return a > b


def divide(a, b):  # 待测试方法
    return a / b


class TestA(unittest.TestCase):

    def setUp(self) -> None:
        print('用例执行前的处理')

    def tearDown(self) -> None:
        print('用例执行后的处理')

    def test_1(self):  # 测试用例
        self.assertTrue(compare(10, 10))

    def test_2(self):  # 测试用例
        self.assertFalse(divide(10, 0))


if __name__ == '__main__':
    unittest.main()
posted @ 2022-06-27 16:42  森声  阅读(850)  评论(0编辑  收藏  举报