一、准备
- 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.***mon.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.***mon.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 --模块,用于执行所有的测试类,并生成测试报告
|–***mon – 包,用于存放公用的工具模块
|–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.***mon.util import get_path
from GAD_webUI.***men.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.***mon.exceptions import NoSuchAttributeException, NoSuchElementException, TimeoutException
from selenium.webdriver.***mon.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.***mon.action_chains import ActionChains
from ***mon.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.***men.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.***', port=25, ssl_port=465):
self.username = username # 发送邮箱用户名
self.passwd = passwd # 发送邮箱授权密码
self.recv = self._format_addr(recv) # 收件人,多个要传list ['a@qq.***','b@qq.***]
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.***',
passwd='这里输入自己163邮箱的授权密码,如 OEGMVWFSIYOWQKBD(这里的授权密码我编造的不可以用哦)',
recv=['这里是接收方的邮箱'], # 可以填多个,多个邮箱地址间要用英文逗号隔开,如['1761xxx@qq.***', '319xxx@qq.***']
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.***mon.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、测试用例执行顺序
- 默认按照 ASCII 码值排序
- 重写 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 ***pare(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(***pare(10, 10))
def test_2(self): # 测试用例
self.assertFalse(divide(10, 0))
if __name__ == '__main__':
unittest.main()