在前一篇文章中,我们介绍了从零开始搭建自动化测试工具的技术选型。接下来,本文将深入探讨如何使用Pytest-BDD框架,结合Selenium和Requests库,实现UI和接口自动化测试的统一,并封装这些工具以适应Gherkin语言编写的测试用例。
selenium封装
初始化实现
为了启动浏览器并进入测试页面,我们首先需要实现初始化步骤。
Gherkin场景:
Feature: UI自动化测试
Scenario: 登录测试
Given 初始化
python实现:
class SeleniumTool():
def __init__(self):
self.options = webdriver.ChromeOptions()
self.options.add_argument("--start-maximized")
self.driver = webdriver.Chrome(options=self.options)
self.driver.get(url)
@pytest.fixture
@given('初始化')
def selenium_tool():
return SeleniumTool()
注意: 我们要测试的URL,可以通过配置文件方式实现,本篇内容先不讲,后面会统一介绍。
获取页面元素
为了简化元素定位,我们将页面元素封装成类,并创建一个方法来获取这些元素:
from selenium import webdriver
from selenium.webdriver.common.by import By
class LoginPageElements:
def __init__(self):
self.by_mapping = {
'ID': By.ID,
'class': By.CLASS_NAME,
'css': By.CSS_SELECTOR,
'name': By.NAME,
'link text': By.LINK_TEXT,
'tag name': By.TAG_NAME,
'xpath': By.XPATH
}
self.login_button = ('xpath', '//*[text()=\'登录\']')
self.account_input = ('ID', 'account')
self.passwd_input = ('ID', 'passwd')
def login_page(self):
login_page = {
'登录按钮': self.login_button,
'用户名输入框': self.account_input,
'密码输入框': self.passwd_input
}
return login_page
def get_element(self, driver: webdriver, element_name):
element_data = self.login_page()[element_name]
by_type, value = element_data
by_method = self.by_mapping[by_type]
element = driver.find_element(by_method, value)
return element
点击操作
Gherkin场景:
When 点击"登录按钮"
python实现:
@when(parsers.parse('点击 "{button_element}"'))
def click_button(selenium_tool, button_element):
# 使用对应的page页面元素
page = LoginPageElements()
element = page.get_element(selenium_tool.driver, button_element)
element.click()
输入操作
Gherkin场景:
When 在元素"用户名输入框"中,输入"admin"
python实现:
@when(parsers.parse('在元素 "{user_name_element}" 中,输入 "{text}"'))
def send_username(selenium_tool, user_name_element, text):
# 使用对应的page页面元素
page = LoginPageElements()
input_element = page.get_element(selenium_tool.driver, user_name_element)
input_element.send_keys(text)
[!NOTE]
- 在实际应用中,我们可能需要为每个页面都创建一个类似的
PageElements
类,以组织和管理该页面的所有元素。- 我们还可以通过配置文件或环境变量来管理测试URL和其他配置信息,使测试更加灵活和可配置。
- 为了保持测试的独立性和可重复性,建议在每个测试场景结束后关闭浏览器或重置浏览器状态。
requests封装
针对于requests的封装,我们将接口请求封装成一个公共函数,在python实现Gherkin场景时,我们只需要调用公共的接口请求函数,可以增加工具的易用性和可维护性。
接口用例Gherkin场景:
Feature: 登录接口测试
Scenario: 登录接口测试
Given 初始化
When 调用"/login"接口
When 使用"post"请求 # post get put delete
When 参数类型"json" # params json form-data form-urlencoded
When 请求头"{'Content-Type': 'application/json'}"
When 参数"{'username': 'test', 'password': 'password'}"
When 校验类型"包含校验" # 校验类型: 包含校验 相等校验 字段类型校验 字段值校验 状态码校验
When 校验文本"成功"
When 校验字段类型为"str" # 字段类型:int str float list dict bool
When 校验字段"loginInfo"
Then 接口调用成功
python实现:
from pytest_bdd import scenarios, given, when, then, parsers
from pytest_bdd.hooks import *
from common.api_tool import api_test
from common.token_api import get_cms_token, do_logout
from common.assert_tool import assert_tool
from common.yaml_tool import read_yaml
from common.assert_tool import analysis_dict
import ast
import os
import logging
test_result_type_list = {
'相等校验': 0,
'包含校验': 1,
'字段类型校验': 2,
'字段值校验': 3,
'状态码校验': 4
}
class ApiTest:
def __init__(self):
self.api = None
self.methods = 'get'
self.data_type = 'params'
self.params = {}
self.headers = {}
self.test_result_type = None
self.result_text = None
self.result_type = None
self.result_data_type = None
self.result_key = None
self.file = None
self.is_logout = False
def call_api(self, test_body):
response = api_test(test_body)
return response
@pytest.fixture
@given('初始化')
def api_tool():
return ApiTest()
@when(parsers.parse('调用 "{url}" 接口'))
def api(api_tool, url):
api_tool.api = url
@when(parsers.parse('使用 "{methods}" 请求'))
def methods(api_tool, methods):
api_tool.methods = methods
@when(parsers.parse('参数类型 "{data_type}"'))
def data_type(api_tool, data_type):
api_tool.data_type = data_type
@when(parsers.parse('请求头 "{headers}"'))
def headers(api_tool, headers):
headers_dict = ast.literal_eval(headers)
for key, value in headers_dict.items():
api_tool.headers[key] = value
@when(parsers.parse('参数 "{params}"'))
def params(api_tool, params):
api_tool.params = ast.literal_eval(params)
@when(parsers.parse('校验类型 "{test_result_type}"'))
def result_test(api_tool, test_result_type):
api_tool.test_result_type = test_result_type_list[test_result_type]
@when(parsers.parse('校验文本 "{result_text}"'))
def result_text(api_tool, result_text):
api_tool.result_text = result_text
@when(parsers.parse('校验字段类型为 "{result_type}"'))
def result_type(api_tool, result_type):
api_tool.result_type = result_type
@when(parsers.parse('校验字段 "{result_key}"'))
def result_key(api_tool, result_key):
api_tool.result_key = result_key
@when(parsers.parse('上传文件 "{files}"'))
def file(api_tool, files):
api_tool.file = files
@then('调用成功')
def asserts(api_tool):
test_data = {'test_result': {'text': api_tool.result_text, 'type': api_tool.test_result_type, 'result_type': api_tool.result_type, 'key': api_tool.result_key}}
test_body = {'URL': api_tool.api, 'method': api_tool.methods, 'data_type': api_tool.data_type, 'headers': api_tool.headers, 'params': api_tool.params, 'file': api_tool.file}
response = api_tool.call_api(test_body)
logging.info('接口返回:' + str(response.text))
if api_tool.is_logout:
do_logout(api_tool.headers['ticket'])
if api_tool.test_result_type is not None:
assert_tool(response, test_data)
上面我们主要把每个步骤的参数拼装成一个完成的接口请求。然后在场景then '调用成功'
去实现调用接口并进行断言。来验证接口测试是否成功。
api_tool 封装
import requests
import os
import json
from common.yaml_tool import read_yaml
from common.token_api import get_token
def api_test(test_body):
config = read_yaml('config/config.yaml')
config_env = config['env']
data_type = test_body['data_type'].lower()
url = config_env + test_body['URL']
method = test_body['method'].lower()
data = test_body['params']
headers = test_body.get('headers', {})
file = test_body.get('file', None)
if data_type not in ['params', 'json', 'form-data', 'form-urlencoded']:
return {'code': '5000', 'msg': '测试脚本暂时不支持的参数类型'}
def make_request():
if data_type == 'params':
return params(url, method, data, headers)
elif data_type == 'json':
return json(url, method, data, headers)
elif data_type == 'form-data':
return formdata(url, method, data, headers, file)
elif data_type == 'form-urlencoded':
return urlencoded(url, method, data, headers)
response = make_request()
return response
def params(url, method, data, headers):
methods = {
'post': requests.post,
'put': requests.put,
'get': requests.get,
'delete': requests.delete
}
try:
response = methods[method](url, params=data, headers=headers)
response.encoding = 'utf-8'
return response
except Exception as e:
return {"code": "5000", "msg": str(e)}
def json(url, method, data, headers):
methods = {
'post': requests.post,
'put': requests.put,
'get': requests.get,
'delete': requests.delete
}
try:
response = methods[method](url, json=data, headers=headers)
return response
except Exception as e:
return json.dumps({'code': '5000', 'msg': '测试脚本报错:' + str(e)})
def urlencoded(url, method, data, headers):
methods = {
'post': requests.post,
'put': requests.put,
'get': requests.get,
'delete': requests.delete
}
try:
response = methods[method](url, data=data, headers=headers)
return response
except Exception as e:
return {'code': '5000', 'msg': '测试脚本报错:' + str(e)}
def formdata(url, method, data, headers, file):
if file:
file_name = file
name = os.path.splitext(file_name)[-1]
file_types = {
'.doc': 'application/msword',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.pdf': 'application/pdf',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.ppt': 'application/vnd.ms-powerpoint',
'.xls': 'application/vnd.ms-excel',
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'.txt': 'text/plain'
}
file_type = file_types.get(name, None)
if file_type is None:
return {'code': '5000', 'msg': '不支持的文件类型'}
path = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
file = [
('file', ('Test'+name, open(path + '/statics/' + file_name, 'rb'), file_type))
]
methods = {
'post': requests.post,
'put': requests.put,
'get': requests.get,
'delete': requests.delete
}
try:
response = methods[method](url, data=data, files=file, headers=headers)
return response
except Exception as e:
return {'code': '5000', 'msg': '测试脚本报错:' + str(e)}
总结
通过本文,我们介绍了如何使用Pytest-BDD框架,结合Selenium和Requests库,实现UI和接口自动化测试的统一封装。我们创建了Selenium和Requests的封装类,简化了测试用例的编写。接下来,我们将继续介绍日志模块、断言模块等相关内容,进一步完善自动化测试工具。
注意: 在实际项目中,请根据具体需求调整代码结构和逻辑。本文提供的示例仅供参考。