目录结构
目录结构 config config.ini 修改目标路由与相关配置 elements 抽象页面空间元素 method 抽象页面方法 TestCase 测试用例 utils Assert 自定义断言chromedriver 浏览器驱动 需匹配版本 非linux 环境无视Driver 初始化drivergraphical 图片相对定位log 日志seleniumBase api封装seleniumOpt opt定义依然使用了常见的po 模式
初始化Driver
根据判断运行环境 选择不同的chrome driver 或者使用
ChromeDriverManager
自动下载
class Driver:@classmethoddef driver(cls):try:# select local or docker# if platform == "Darwin":# url = Config().get_conf("dockerHub", "url")# driver = webdriver.Remote(url, desired_capabilities=capabilities(),# options=options())# # else:if platform.system() == "Linux":executable_path = os.path.join(os.path.dirname(__file__), "chromedriver")driver = webdriver.Chrome(executable_path=executable_path, options=options())else:executable_path = ChromeDriverManager().install()driver = webdriver.Chrome(executable_path=executable_path, options=options())driver.implicitly_wait(20)log.info(f"driver:{driver.name}")return driverexcept BaseException as e:log.error(e)raise e
自定义
driver
的options
配置
def options():"""浏览器设置:return:"""_options = ChromeOptions()# # 设置chrome为手机界面# mobileEmulation = Config().get_conf("seleniumOpt", "mobileEmulation")# if mobileEmulation == 'true':#mobile_emulation = {"deviceName": "Galaxy S5"}#_options.add_experimental_option("mobileEmulation", mobile_emulation)if platform.system() == 'Darwin':passelse:_options.add_argument('--headless')_options.add_argument('--no-sandbox')_options.add_argument('--disable-gpu')_options.add_argument('--disable-dev-shm-usage')return _options
Base类
封装常用API 把
driver
实例化传入查找元素尽量使用显示等待
expected_conditions
使用了
Faker
可随意生成测试数据查找元素失败 使用
allure
上传截图
class SeleniumBaseConfig:faker = Faker(locale="zh_CN")Assert = Assertlog = get_log()def __init__(self, driver: webdriver):""":param driver: 驱动"""self.driver = driverdef find_element(self, locator: tuple, timeout=5):"""find_element:param locator: (xpath,xxx)(id,xxx):param timeout: timeout:return:"""try:element = WebDriverWait(self.driver, timeout).until(EC.presence_of_element_located(locator))self.log.info(f"[{self.find_element.__name__}] locator: {locator} element: {str(element)}")return elementexcept WebDriverException as e:self.log.error(e)allure.attach(self.get_png(), "异常截图", allure.attachment_type.PNG)allure.attach("未找到: {}, 详细信息如下: {}".format(locator, e))raise e
自定Assert
很常规 没什么可说的
class Assert:@staticmethoddef assert_unequal(exp, res):try:log.info("ASSERT UNEQUAL: EXPECT IS [{}] RESULT IS [{}]".format(exp, res))assert exp != resreturn Trueexcept AssertionError as e:log.error("ASSERT UNEQUAL ERROR: EXPECT IS [{}] RESULT IS [{}]".format(exp, res))raise e
关于页面元素
根据op 依旧对页面抽象成类 并继承
SeleniumBaseConfig
元素类1一对1配套
class RegisterMethod(SeleniumBaseConfig):def __init__(self, driver):super().__init__(driver)def register(self, mobile: str):"""注册:param mobile:return:"""self.send_keys(RegisterLogin.INPUT_NUMBER, mobile)self.click(RegisterLogin.NEXT_STEP_BUTTON)time.sleep(1)
对应元素类
class Register:# ================== 公共 ================# 下一步NEXT_STEP_BUTTON = ("xpath", "//button")# 返回RETURN = ('xpath', '//p[@class="return-prev-step"]')# input 错误信息INPUT_ERROR_INFO = ("xpath", '//p[@class="default-input-error"]')# ================== home界面 ================CREATE_BUTTON = ("xpath", "//div[text()='创建']") # 创建buttonJOIN_BUTTON = ("xpath", "//div[text()='加入']") # 创建buttonINPUT_TEAM_NAME = ("xpath", '//input[@placeholder="请输入团队名称"]') # 输入团队名称INPUT_YOUR_NAME = ("xpath", '//input[@placeholder="请输入你的姓名"]') # 输入个人名称
测试用例
@pytest.mark.P3@allure.title("创建新团队:姓名输入汉字、英文、数字字符")@pytest.mark.flaky(reruns=1, reruns_delay=2)def test_create_team_username(self):"""姓名输入汉字、英文、数字字符1.register2.create team3.create_team_username"""pics = []self.driver.register(self.mobile)exp = "true"res = self.driver.check_button_disabled()self.driver.allure_report(self.test_create_team_username, pics, exp, res)self.driver.Assert.assert_equal(exp, res)
Config 配置
一般配置
host
等数据 使用ConfigParser
读取写入
config.ini
[loginParam]username = usernamepassword = 123456[domain]cm = xx
class Config:def get_conf(self, title, value):"""read .ini:param title::param value::return:"""log.info("{} : {}:{}".format(self.get_conf.__name__, title, value))return self.config.get(title, value)def set_conf(self, title, value, text):"""change .ini:param title::param value::param text::return:"""log.info("{} : {}:{}:{}".format(self.set_conf.__name__, title, value, text))self.config.set(title, value, text)with open(self.config_path, 'w+') as f:return self.config.write(f)
main 启动
使用
sys.argv
获取命令行自定义参数
if len(sys.argv) > 3:runCase = sys.argv[1] # P1 P2 P3 ALL 指定运行的用例等级if runCase != "ALL":case_path = f"-m={runCase}"xml = sys.argv[2] # 报告路径if xml:xml_report_path = xmlBUILD_NUMBER = sys.argv[3]args = ["-s", "-v", "-n=auto", case_path, '--alluredir', xml_report_path, '--clean-alluredir']pytest.main(args=args)
图片相对定位 获得坐标
场景
canvas 页面 无元素可定位
使用
先截取想要点击的位置图片 保存到本地
find_me
传入driver
对象通过
center_x
与center_y
属性方法 获得相对坐标
class GraphicalLocator():def __init__(self, img_path):self.locator = img_path# x, y position in pixels counting from left, top cornerself.x = Noneself.y = Noneself.img = cv2.imread(img_path)self.height = self.img.shape[0]self.width = self.img.shape[1]self.threshold = None@propertydef center_x(self):return self.x + int(self.width / 2) if self.x and self.width else None@propertydef center_y(self): return self.y + int(self.height / 2) if self.y and self.height else Nonedef find_me(self, drv): # Clear last found coordinatesself.x = self.y = None# Get current screenshot of a web pagescr = drv.get_screenshot_as_png()# Convert img to BytesIOscr = Image.open(BytesIO(scr))# Convert to format accepted by OpenCVscr = numpy.asarray(scr, dtype=numpy.float32).astype(numpy.uint8)# Convert image from BGR to RGB formatscr = cv2.cvtColor(scr, cv2.COLOR_BGR2RGB)# Image matching works only on gray images# (color conversion from RGB/BGR to GRAY scale)img_match = cv2.minMaxLoc(cv2.matchTemplate(cv2.cvtColor(scr, cv2.COLOR_RGB2GRAY),cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY),cv2.TM_CCOEFF_NORMED))# Calculate position of found elementself.x = img_match[3][0]self.y = img_match[3][1]# From full screenshot crop part that matches template imagescr_crop = scr[self.y:(self.y + self.height),self.x:(self.x + self.width)]# Calculate colors histogram of both template# and matching images and compare themscr_hist = cv2.calcHist([scr_crop], [0, 1, 2], None,[8, 8, 8], [0, 256, 0, 256, 0, 256])img_hist = cv2.calcHist([self.img], [0, 1, 2], None,[8, 8, 8], [0, 256, 0, 256, 0, 256])comp_hist = pareHist(img_hist, scr_hist,cv2.HISTCMP_CORREL)# Save treshold matches of: graphical image and image histogramself.threshold = {'shape': round(img_match[1], 2), 'histogram': round(comp_hist, 2)}# Return image with blue rectangle around matchreturn cv2.rectangle(scr, (self.x, self.y),(self.x + self.width, self.y + self.height),(0, 0, 255), 2)
pytest
pytest 有很多好用的功能、比如失败重跑、多进程并行用例、
fixture
测试用例参数化等等
详见 gitHUB-SeleniumUIAuto 中的 readme