我们已经准备好了,你呢?

2026我们与您携手共赢,为您的企业形象保驾护航!

[id_[[id_16[]]][]]

的名称

[]

文件名

哪个文件打印的这条日志

line

行号

哪一行打印的这条日志

级别

日志的级别,注意是级别的name

内容

我们打印的日志内容

日志文件

保存到哪个日志文件

[[]9590]

一是存放目录问题,我们这里使用了固定目录,所以问题不大。

日志的分割与滚动处理是关键问题,每日持续集成作业会产生大量用例,进而生成海量日志,堆积如山。若认为日志具有价值,则可搭建ELK系统,将日志提取并妥善存储以供分析。反之,若认为日志无实际用途,则可在保存数日后予以删除。无论如何,确保日志的分割与滚动机制得以有效实施。

幸运的是,那些大佬们早已预料到了这一点,该模块本身就具备这样的功能,只需进行简单的设置即可。

开始操作,加载库函数,提取项目的基础路径信息,同时获取.ini文件中的日志设置,最终将日志文件的存储完整路径组合完成。

import[id_1705551741]
import logging
from Conf.Config import log_cfg
_BaseHome = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
_log_level = eval(log_cfg['log_level'])
_log_path = log_cfg['log_path']
_log_format = log_cfg[[id_2021344278]]
_log_file = os.path.join(_BaseHome, _log_path, 'log.txt')

请注意,上述代码中使用了eval函数,若未包含此函数,所获取的将是一个字符串,无法直接使用。只有通过执行eval函数,才能将其转换为定义的对象。

再配置日志,引入dler这个东东,这是实现滚动日志的。

from logging.handlers import TimedRotatingFileHandler
def log_init():
    logger = logging.getLogger('main')
    logger.setLevel(level=_log_level)
    formatter = logging.Formatter(_log_format)
    handler = TimedRotatingFileHandler(filename=_log_file, when="D", interval=1, backupCount=7)
    handler.setLevel(_log_level)
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    console = logging.StreamHandler()
    console.setLevel(_log_level)
    console.setFormatter(formatter)
    logger.addHandler(console)

在这份日志文档中,新增了两个输出功能,它们分别负责将日志信息输出至日志文件以及终端显示,这两个输出的实现方式存在差异。

dler的参数简介:

参数意义说明

日志文件

没啥好说的

[]

切割条件

按周(W)、天(D)、时(H)、分(M)、秒(S)切割

间隔

即是采用若干个“当”来划分,这里的“当”指的是W,若数值为3,即表示每三周进行一次分割。

日志备份数量

仅保留一定数量的日志文档,一旦超过此数,便将最旧的记录予以清除,以此实现日志的循环删除。

我这里配置的是每天生成1个日志文件,保留7天的日志。

日志就做好了,试一下效果。

log_init()
logger = logging.getLogger('main')
logger.info('log test----------')

运行结果:

2021-03-15 21:53:41,972 - main - Log.py[line:49] - INFO - log test----------

其它文件使用日志:

在main.py文件中,首先导入该模块,随后在程序启动之初进行初始化操作,这样日志系统便会被正确配置。

再在各个要使用日志的文件中,直接按下面这种方式使用:

import logging
logger = logging.getLogger('main.jd')

在关注各自模块的同时,只需在main之后附加相应的模块名称,即可实现模块间的有效区分。

到这里日志功能就完成了。

为大家提供截图操作,方便大家使用。截图操作既可以在用例中通过内置的截图功能完成,也可以自行创建一个公共的截图工具。此截图功能是基于PIL库实现的。

from PIL import ImageGrab
首先,需确定截图文件的存储位置;为此,在Log目录中创建一个名为Screen的子目录,并将截图按照每日进行分类存放。
_today = time.strftime("%Y%m%d")
_screen_path = os.path.join(_BaseHome, _log_path, 'Screen', _today)
#再使用PIL的ImageGrab实现截图
def screen(name):
    t = time.time()
    png = ImageGrab.grab()
    if not os.path.exists(_screen_path):
        os.makedirs(_screen_path)
    image_name = os.path.join(_screen_path, name)
    png.save('%s_%s.png' % (image_name, str(round(t * 1000))))  # 文件名后面加了个时间戳,避免重名

使用此法即可实现屏幕截图,任务圆满完成。实际上,截图生成的文件同样需要设置自动清除,这项功能待后续时间再进行开发。

3. 读取EXCEL实现(data)

接下来,我们将开发一个专门用于读取EXCEL文件数据的功能模块,该模块的核心目的是为了读取测试数据,从而实现数据驱动的测试流程。

处理Excel文件时,我发现很多人偏爱使用xlrd和xlwt,但对我来说,这些方法都显得有些繁琐。

我们用来干,一句话的事情,搞那么多干吗,用就是要快。

在Comm文件夹中,创建一个名为data.py的新文件,该文件专门用于数据操作。在此文件中,我们直接使用函数来读取Excel文件,同时保留了其原始的众多参数设置。不过,我们对其进行了小小的调整,最终将读取结果转换成了字典格式,以便于后续的使用。

import pandas as pd
def read_excel(file, **kwargs):
    data_dict = []
    try:
        data = pd.read_excel(file, **kwargs)
        data_dict = data.to_dict('records')
    finally:
        return data_dict

在同一目录中随意放置一个Excel文件,输入相关数据,然后测试其功能。该Excel文件包含两页数据,具体如下:

在这里插入图片描述

如下:

调用我们写好的方法,打印数据:

sheet1 = read_excel('baidu_fanyi.xlsx')
sheet2 = read_excel('baidu_fanyi.xlsx', sheet_name='Sheet2')
print(sheet1)
print(sheet2)

运行结果如下:

[{'req.q': '计算机\n计算机', 'req.from': 'zh', 'req.to': 'en', 'res.from': 'zh', 'res.to': 'en', res.trans_result中的第一个元素的源字段。: '计算机', res.trans_result中的第一个元素的dst属性: 'computer', res.trans_result中第一个元素的源字段: '计算机', res.trans_result中第1个元素的dst属性: 'computer'}, 
 {'req.q': 计算机期望值, 'req.from': 'en', 'req.to': 'zh', 'res.from': 'en', 'res.to': 'zh', 'res.trans_result.0.src': 'computer', 'res.trans_result.0.dst': '计算机', 'res.trans_result.1.src': 'expected value', 'res.trans_result.1.dst': '苹果'}]
[{'req.q': '计算机', 'req.from': 'zh', 'req.to': 'en', 'res.from': 'zh', 'res.to': 'en'}, 
{'req.q': 'computer', 'req.from': 'en', 'req.to': 'zh', 'res.from': 'en', 'res.to': 'zh'}]

每一页的数据均已被成功读取,并且每一条记录均呈现为字典结构,这使得我们能够通过键值对的方式轻松访问所需信息。

可直接进行数据运算,例如通过多列来生成加密的签名,编写动态内容等,其操作步骤亦相当简便。例如,在数据集中新增一列名为“sign”,并设定其值等于“req.from”列内容加上“.aaaa.”再加上“req.to”列内容,下面我将为大家进行演示。

data = pd.read_excel('baidu_fanyi.xlsx')
data['sign'] = data["req.from"] +'.aaaaa.' + data["req.to"]
data_dict = data.to_dict('records')
print(data_dict)

运行结果:

[{'req.q': '计算机\n计算机', 'req.from': 'zh', 'req.to': 'en', 'res.from': 'zh', 'res.to': 'en', 'res.trans_result.0.src': '计算机', 'res.trans_result.0.dst': 'computer', 'res.trans_result.1.src': '计算机', 'res.trans_result.1.dst': 'computer', 'sign': 'zh.aaaaa.en'}, 
{'req.q': 'computer\nexpected value', 'req.from': 'en', 'req.to': 'zh', 'res.from': 'en', 'res.to': 'zh', 'res.trans_result.0.src': 'computer', 'res.trans_result.0.dst': '计算机', 'res.trans_result.1.src': 'expected value', 'res.trans_result.1.dst': '苹果', 'sign': 'en.aaaaa.zh'}]

观察发现,新增了一列名为“sign”的数据,其数值是系统自动基于每行数据计算得出的。这一功能对于我们依赖数据来驱动分析,特别是在计算动态数值方面,显得尤为实用。在我目前的操作中,并未涉及动态计算,仅仅是进行读取。若大家需要执行计算,则需要自行编写相应的计算方法。

它能够直接读取众多主流数据库,后续的拓展工作也十分便捷,我们一直以来都依赖并使用它。

4. 邮件发送实现(Email)

实现邮件功能,用于发送测试报告。使用的模块实现。

先在Conf目录下的.ini中添加好邮件相关的配置:

[smtp]
host = smtp.163.com
port = 465
user = example@163.com
passwd = password
[email]
sender = example@163.com
receivers = example@qq.com, example@163.com

再在.py中将它们取到变量中放好:

smtp_cfg = config['smtp']
email_cfg = config['email']

在Comm文件夹中,首先创建一个名为Email的文件,并着手编写代码。此代码实现了对邮件主题、内容以及多个附件的定义,同时对单个附件的体积和附件的总数进行了限制。具体代码如下:

import smtplib
import os
import logging
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.header import Header
from Conf.Config import smtp_cfg, email_cfg
_FILESIZE = 20  # 单位M, 单个附件大小
_FILECOUNT = 10  # 附件个数
_smtp_cfg = smtp_cfg
_email_cfg = email_cfg
_logger = logging.getLogger('main.email')
class Email:
    def __init__(self, subject, context=None, attachment=None):
        self.subject = subject
        self.context = context
        self.attachment = attachment
        self.message = MIMEMultipart()
        self._message_init()
    def _message_init(self):
        if self.subject:
            self.message['subject'] = Header(self.subject, 'utf-8')  # 邮件标题
        else:
            raise ValueError("Invalid subject")
        self.message['from'] = _email_cfg['sender']  # from
        self.message['to'] = _email_cfg['receivers']  # to
        if self.context:
            self.message.attach(MIMEText(self.context, 'html', 'utf-8'))  # 邮件正文内容
        # 邮件附件
        if self.attachment:
            if isinstance(self.attachment, str):
                self._attach(self.attachment)
            if isinstance(self.attachment, list):
                count = 0
                for each in self.attachment:
                    if count <=[id_1922710245]:
                        self._attach(each)
                        count += 1
                    else:
                        _logger.warning(附件不仅仅是, _FILECOUNT)
                        break
    def _attach(self, file):
        if os.path.isfile(file) and os.path.getsize(file) <= _FILESIZE * 1024 * 1024:
            attach = MIMEApplication(open(file, 'rb').read())
            attach.add_header('Content-Disposition', 'attachment', filename=os.path.basename(file))
            attach["Content-Type"] = 该数据类型标识为文件内容,具体表现为二进制流格式。
            self.message.attach(attach)
        else:
            _logger.error(附件不存在或超过%s兆:请检查%s。 % (_FILESIZE, file))
    def send_mail(self):
        s = smtplib.SMTP_SSL(_smtp_cfg['host'], int(_smtp_cfg['port']))
        result = True
        try:
            s.login(self._smtp_cfg['user'], self._smtp_cfg['passwd'])
            s.sendmail(self._smtp_cfg['sender'], self._smtp_cfg['receivers'], self.message.as_string())
        except smtplib.SMTPException as e:
            result = False
            _logger.error('Send mail failed', exc_info=True)
        finally:
            s.close()
        return result

邮件初始化发送时的调用方式如下:

mail = Email(title, context, file)
send = mail.send_mail()
print(send)

返回结果为True则发送成功,否则发送失败。

四、WEB UI自动化

WEB UI自动化测试的实施,主要依靠特定工具来完成。这一过程通过将页面对象(PO)、测试所需的数据以及业务处理逻辑三者进行有效分离,从而实现自动化测试的目标。

另一个核心思想是鼓励测试人员掌握原生方法,减少不必要的封装。理由很明确,我们不是要让测试人员去学习这个框架,而是让他们专注于技能的提升,以便将来换工作时能够维持生计。一旦过度封装,测试人员将不得不学习这个框架,但毕业后却可能无法应用所学,这不是对他人造成伤害吗?框架的真正作用在于推动对象、数据和业务逻辑的协同运作,从而提升测试人员的工作效率。

以京东的搜索爬虫为案例,我们可以观察并学习三者之间的构建关系:首先,在京东的主页面上输入“电脑”进行搜索;接着,获取搜索到的结果;最后,将这些结果进行保存。

1. 页面PO对象配置

进入京东官网,识别页面上的搜索栏及搜索按钮,详尽核实它们的定位策略,并记录下每个元素的操作方法。

接下来,我们需要创建这个页面对象,具体操作是在“Page”目录下新增一个以“jd”命名的子目录,随后在该子目录中创建一个名为“jd.py”的文件,该文件的主要作用是定义京东商城的主页面。

from selenium.webdriver.common.by import By
page_url = 'https://www.jd.com'
elements = [
    {'name': 'search_ipt', 'desc': '搜索框点击', 'by': (By.ID, u'key'), 'ec': 元素定位存在, 'action': 'send_keys()'},
    {'name': 'search_btn', 'desc': '搜索按钮点击', 'by': (By.CLASS_NAME, u'button'), 'ec': 'presence_of_element_located', 'action': 'click()'},
]

每个元素都拥有其自身操作的唯一标识符。一个元素可能因操作的不同而需要设定多个标识符,然而,大多数情况下仅需设定一个即可。

desc:元素+操作的描述。

by:元素的定位方式,使用的原生定位方式,不自己定义封装。

ec: 等待元素出现的方式,这个暂时未用。

:元素的对应操作。使用原生的动作方法,不自己定义封装。

京东商城主页面现在只用到这两个,就只定义这两个。

搜索结果页面,定义如下:

from selenium.webdriver.common.by import By
page_url = 请勿访问此链接:https://search.jd.com/。
elements = [
    {'name': 'result_list', 'desc': '结果列表', 'by': (By.CLASS_NAME, u'gl-item'), 'ec': 所有元素的存在性被严格限制。, 'action': None},
    {'name': 'price', 'desc': '价格', 'by': (By.XPATH, 选取具有“p-price”类名的div元素中的strong标签内的i标签。), 'ec': 'presence_of_element_located', 'action': 'text'},
    {'name': 'pname', 'desc': '描述', 'by': (By.XPATH, 该选择器定位到具有特定类名“p-name p-name-type-2”的div元素内部的a标签中的em子元素。), 'ec': 'presence_of_element_located', 'action': 'text'}
]

2. 实现基类

基类的实现理念在于减少封装层次,力求让测试人员能够直接调用原始方法,与那些将所有功能都封装在一起的框架形成鲜明对比。

因此,我所理解的定义是:依据业务逻辑(即测试用例)所规定的要素,对输入的数据进行辅助,帮助实现该要素的定位与操作,仅限于此。

当然如果去封装各种东西也是可以的,直接在里面加就行了。

在Page目录下,新建.py,开始撸代码:

from selenium.webdriver.common.by import By
from selenium import webdriver
import os
import importlib
import logging
SimpleActions = ['clear()', 'send_keys()', 'click()', 'submit()', 'size', 'text', 'is_displayed()', 'get_attribute()']
logger = logging.getLogger('main.page')
class Page(object):
    def __init__(self, driver, page):
        self.driver = driver
        self.page = page
        self.elements = get_page_elements(page)
        self.by = ()
        self.action = None
    def _get_page_elem(self, elem):
        # 获取定位元素的 by,以及操作action
        for each in self.elements:
            if each['name'] == elem:
                self.by = each['by']
                if 'action' in each and each['action'] is not None:
                    self.action = each['action']
                else:
                    self.action = None
    def oper_elem(self, elem, args=None):
        self._get_page_elem(elem)
        cmd = self._selenium_cmd('find_element', args)
        return eval(cmd)
    def oper_elems(self, elem, args=None):
        self._get_page_elem(elem)
        cmd = self._selenium_cmd('find_elements', args)
        return eval(cmd)
    def _selenium_cmd(self, find_type='find_element', args=None):
        在执行selenium查找操作时,若需定位单个元素,应使用`find_element`命令;若需定位多个元素,则应采用`find_elements`命令。
        cmd = 'self.driver.' + find_type + '(*self.by)'
        if self.action:
            if self.action in SimpleActions:
                cmd = cmd + '.' + self.action
                if args:
                    cmd = cmd[:-1] + 'args' + ')'
        return cmd
def get_page_elements(page):
    对页面定义文件进行动态加载,从中提取所定义的元素列表,称之为elements。
    elements = None
    if page:
        try:
            m = importlib.import_module(page)
            elements = m.elements
        except Exception as e:
            logger.error('error info : %s' %(e))
    return elements

这里主要涉及三种技术手段,首先是通过动态加载特定的PO对象来获取元素列表,其次是针对获取到的元素列表进行筛选,以定位到需要操作的特定元素,最后则是将原始命令进行组合,将测试数据嵌入到执行动作中。

其他的操作相对直接,只需执行预先拼接好的命令,并将输出结果反馈出来。

需要注意的是,对于某些较为复杂的操作,不能仅仅通过简单拼接命令来完成,需要采取特殊方法进行处理,目前这部分尚未完成;而一些简单的命令也尚未全部列出。后续将会逐步补充。

3. 写业务测试用例

下面开始写测试用例。

在目录中,创建一个新的子目录。接着,在下方再设立一个子目录,专门用于存放测试数据。此外,还需建立一个名为“Case”的目录,该目录将用于保存用例脚本。目录的布局安排如下:

准备测试数据:

准备一份excel数据(.xlsx),存放在//jd下:

:搜索的关键字

count:搜索结果总数,只抓了一页,应该是60个

实现业务用例:

在/Case/jd下新建一个文件:.py,开始写用例脚本。

用例使用结合DDT来实现,具体代码如下:

import os
import unittest
import ddt
import logging
from selenium import webdriver
from time import sleep
from Page.basePage import Page
from Comm.Log import screen
from Comm.data import read_excel
from main import TestCasePath
logger = logging.getLogger('main.jd')
# 读取测试数据
file = os.path.join(TestCasePath, Model1目录下,位于TestData文件夹中,名为test_jd_desktop的Excel文件。)
test_data = read_excel(file)
PO_jd = 'Page.jd.jd'
PO_search = 'Page.jd.search_jd'
@ddt.ddt  # 数据驱动
class TestJdSearchDesktop(unittest.TestCase):
    """京东搜索测试"""
    def setUp(self):
        self.driver = webdriver.Chrome()
        self.count = 0
        self.result = []
    @ddt.data(*test_data) # 数据驱动传具体数据
    def testJdSearchDesktop(self, test_data):
        """京东搜索测试--电脑"""
        url = 'https://www.jd.com'
        keyword = test_data['keyword']
        wait = self.driver.implicitly_wait(5)
        try:
            self.driver.get(url)
            # 实例化jd主页面
            jd = Page(self.driver, PO_jd)
            # 实例化jd搜索结果页面
            jd_search = Page(self.driver, PO_search)
            wait
            # jd主页面的搜索框元素中输入关键字
            jd.oper_elem('search_ipt', keyword)
            wait
            # 操作jd主页面的搜索按钮元素
            jd.oper_elem('search_btn')
            sleep(1)
            self.driver.execute_script(执行滚动操作,使窗口滚动至页面顶部,并调整至文档内容的底部位置。)
            sleep(1)
			# jd搜索结果页面,获取结果列表
            lis = jd_search.oper_elems('result_list')
			在检索到的数据列表中,逐一提取商品的价格信息以及商品名称,之后将这些信息录入到EXCEL表格中,后续的操作就不再赘述。
            for each in lis:
                self.count += 1
                page_each = Page(each, PO_search)
                price = page_each.oper_elem('price')
                name = page_each.oper_elem('pname')
                self.result.append([name, price])
            sleep(1)
        except Exception as E:
            logger.error('error info : %s' % (E))
            screen(test_data['keyword'])
		# 判断是不是取到了60个商品
        self.assertEqual(test_data['count'], self.count)
    def tearDown(self):
        self.driver.quit()

五、实现主程序

主程序主要负责对用例进行编排,执行这些用例,并据此生成相应的报告,同时负责发送测试报告的邮件。

组织用例和执行用例都直接用;

生成报告,采用;

下面开始撸main.py的代码:

import unittest
import os
import time
import logging
from Comm.Email import Email
from Comm.Log import log_init
from BeautifulReport import BeautifulReport
# 定义各目录
ProjectHome = os.path.split(os.path.realpath(__file__))[0]
PageObjectPath = os.path.join(ProjectHome, "Page")
TestCasePath = os.path.join(ProjectHome, "Testcase")
ReportPath = os.path.join(ProjectHome, "Report")
#对测试结果关键信息进行汇总,做为邮件正文
def summary_format(result):
    summary = "\n" + u"

测试结果汇总信息

"
+ "\n" + \ u"

开始时间: " + result['beginTime'] + u"

"
+ "\n" + \ u"

运行时间: " + result['totalTime'] + u"

"
+ "\n" + \ u"

执行用例数: " + str(result['testAll']) + u"

"
+ "\n" + \ u"

通过用例数: " + str(result['testPass']) + u"

"
+ "\n" + \ u"

失败用例数: " + str(result['testFail']) + u"

"
+ "\n" + \ u"

忽略用例数: " + str(result['testSkip']) + u"

"
+ "\n" return summary # 发送邮件 def send_email(file, context): title = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + '自动化测试结果' mail = Email(title, context, file) send = mail.send_mail() if send: print('测试报告邮件发送成功') else: print('测试报告邮件发送失败') # 加载测试用例 def get_suite(case_path=TestCasePath, rule="test_*.py"): """加载所有的测试用例""" unittest_suite = unittest.TestSuite() discover = unittest.defaultTestLoader.discover(case_path, pattern=rule, top_level_dir=None) for each in discover: unittest_suite.addTests(each) return unittest_suite 执行测试用例,制作测试结果报告,随后提供报告的文件存储位置以及邮件的具体内容。 def suite_run(unittest_suite): """执行所有的用例, 并把结果写入测试报告""" run_result = BeautifulReport(unittest_suite) now = time.strftime("%Y%m%d%H%M%S", time.localtime()) filename = now + '_report.html' run_result.report(filename=filename, description=now, report_dir=ReportPath) rpt_summary = summary_format(run_result.fields) return os.path.join(ReportPath, filename), rpt_summary # 主程序,加载用例,执行用例,发送邮件 if __name__ == "__main__": suite = get_suite() report_file, report_summary = suite_run(suite) print(report_summary) send_email(report_file, report_summary)

运行主程序,就可以把WEB UI自动化跑起来了。

六、API 自动化

API自动化测试,可以选择使用特定的库来执行,亦或是通过将PO对象、测试数据以及业务逻辑三者进行有效分离的方法来达成。

以百度提供的通用翻译服务为例,该服务对公众开放且不收取个人用户的费用,用户可以自行申请获取使用权限。

1. API对象配置

在APIs目录中创建一个名为fanyi的新文件夹,随后在fanyi文件夹内建立一个名为baidu的文件。

在此处设定了百度通用翻译的接口,并采用了大家所熟知的JSON格式进行数据传输。


"""百度通用翻译接口""" 
API_NAME = 'fanyi'
# 地址信息
uri_scheme = 'http'
endpoint = 'api.fanyi.baidu.com'
resource_path = 不允许对“/api/trans/vip/translate”这一接口进行任何修改。
url = uri_scheme + u'://' + endpoint + resource_path
# 保持不变的参数
_from = 'en'
_to = 'zh'
# 请求消息参数
req_param = {
    "q": "",  # 请求翻译 query, UTF-8
    "from": _from,  # 翻译源语言
    "to": _to,  # 翻译目标语言
    "appid": "",  # APP ID
    "salt": "",  # 随机数
    "sign": "",  # 签名,appid+q+salt+密钥 的MD5值
}
# 响应消息参数
res_param = {
    "from": _from,
    "to": _to,
    "trans_result": [
        {
            "src": 您好,欢迎光临!这里是第一段内容。,
            "dst": "你好,世界!这是第一段。"
        },
        {
            "src": "This is 2nd paragraph.",
            "dst": "这是第二段。"
        }
    ]
}

2.实现基类

基类,主要是将数据、API对象、测试用例三者连起来;

在APIs目录下,新建.py,代码如下:

import logging
import random
import importlib
import copy
import json
import unittest
from hashlib import md5
from ipaddress import ip_address
from Comm.compare import json_compare
logger = logging.getLogger('main.api')
req_prefix = 'req.'
res_prefix = 'res.'
def _separate_data(data, prefix='req.'):
    pfx = prefix
    result = {}
    for key, value in data.items():
        if key.startswith(pfx):
            req_key = key[len(pfx):]
            result[req_key] = value
    return result
def _get_cmd(key, dict_name='payload'):
    separator = '.'
    cmd = dict_name
    if separator in key:
        data_key = key.split(separator)
        for each in data_key:
            if each.isdigit():
                cmd = cmd + '[' + each + ']'
            else:
                cmd = cmd + '[\'' + each + '\']'
        cmd = cmd + ' = value'
    else:
        cmd = cmd + '[key] = value'
    return cmd
def check_result(unittest_testcase, x, y):
    只有当x和y完全一致时,才能成功通过;若两者中任意一个存在差异,则系统将判定为失败。建议用户在编写测试用例时,对结果进行仔细核对。
    testcase = unittest_testcase
    diff = json_compare(x, y)
    testcase.assertEqual(x, y)
class BaseAPI(object):
    def __init__(self, api):
        self.api = api
        self.api_name = None
        self.url = ''
        self.req_template = {}
        self.res_template = {}
        self._get_api_param()
    def _get_api_param(self):
        执行动态导入API配置文档操作,从中提取所定义的API相关参数。
        try:
            m = importlib.import_module(self.api)
            self.api_name = m.API_NAME
            self.url = m.url
            self.req_template = m.req_param
            self.res_template = m.res_param
        except Exception as e:
            logger.error('error info : %s' % e)
    def payload(self, data=None):
        payload = copy.deepcopy(self.req_template)
        if data:
            req_pre = '.'.join([self.api_name, req_prefix])
            req_data = _separate_data(data, req_pre)
            for key, value in req_data.items():
                cmd = _get_cmd(key, 'payload')
                exec(cmd)
        return payload
    def load_expected(self, data=None):
        expected = copy.deepcopy(self.res_template)
        if data:
            res_pre = '.'.join([self.api_name, res_prefix])
            res_data = _separate_data(data, res_pre)
            for key, value in res_data.items():
                cmd = _get_cmd(key, 'expected')
                exec(cmd)
        return expected

这里面的思路是:

在动态加载API对象并获取API请求参数模板及响应参数模板的过程中,需从测试数据中提取与API请求相关的信息,例如以API名称加“.req”为前缀的数据(如fanyi.req.q),并将其填充至模板中;若测试数据中缺少此类信息,则直接使用模板中的数据。同样,在加载预期结果时,需从测试数据中提取与API响应相关的信息,例如以API名称加“.res”为前缀的数据(如fanyi.res..0.src),并将其填充至模板;若测试数据中缺少此类信息,则同样使用模板中的数据。提供json比较的方法;提供了一个随机。

大家具体观察一下便可知晓。若想进一步进行封装处理,也是可以的,例如进行数据生成,完成数据配置后直接发送,获取结果后进行比对等。然而,在此过程中,建议不要过度进行封装。

附json比较的方法:

import json_tools
def json_compare(x, y):
    diff = json_tools.diff(x, y)
    if diff:
        for action in diff:
            if 'add' in action:
                print('++增加元素:', action['add'], ' 值:', action['value'])
            elif 'remove' in action:
                print('--删除元素:', action['remove'], ' 值:',  action['prev'])
            elif 'replace' in action:
                print('**修改元素:', action['replace'], ' 值:', action['prev'], '-->', action['value'])
    return diff

3.测试用例

在本项目中构建API模块,并在该模块下创建Case子目录,用于存放测试用例,同时设立另一个子目录用于存储相关数据,具体目录结构如下:

在这里插入图片描述

定义测试数据:

测试数据需遵循特定格式进行编排,具体要求是:每个参数的开头需包含API名称,之后通过点号“.”进行连接,接着用“res”或“req”来标识是响应内容还是请求信息,最后紧跟具体参数内容。若参数存在层级关系,则各级参数间同样以点号“.”相连。

在这里插入图片描述

测试用例脚本:

仍然用和ddt来实现。

import os
import unittest
import ddt
import random
import json
import requests
from time import sleep
from Comm.data import read_excel
from Comm.encryption import make_md5
from main import TestCasePath
from APIs.base_api import BaseAPI, check_result
启动针对普通用户的百度翻译服务,并配置相应的appid及appkey。
app_id = your appid
app_key = your appkey
# 获取测试数据
file = os.path.join(TestCasePath, 该文件位于API目录下的TestData子目录中,名为“baidu_fanyi.xlsx”。)
test_data = read_excel(file)
api = 'APIs.fanyi.baidu'
@ddt.ddt
class TestBaiduFanyi(unittest.TestCase):
    """百度翻译接口测试"""
    def setUp(self):
        self.api = BaseAPI(api)
    @ddt.data(*test_data)
    def test_baidu_fanyi(self, test_data):
        """百度翻译接口测试"""
        api = self.api
        构建测试数据集,这些参数是动态的,在此处进行计算。
        test_data['fanyi.req.appid'] = app_id
        salt = random.randint(32768, 65536)
        test_data['fanyi.req.salt'] = salt
        sign = make_md5(app_id + test_data['fanyi.req.q'] + str(salt) + app_key)
        test_data['fanyi.req.sign'] = sign
        # Build request
        headers = {'Content-Type': 该编码表示的是一种用于网页表单数据编码的MIME类型,其具体格式为将表单字段名与值通过等号连接,字段之间以与号分隔。}
        payload = api.payload(test_data )
        # Send request
        r = requests.post(api.url, params=payload, headers=headers)
        result = r.json()
        expected = api.load_expected(test_data)
        self.assertEqual(r.status_code, 200)
        check_result(self, expected, result) # 简单的模板验证,大家最好自己写验证。
        sleep(0.5)

然后运行主程序,API自动化测试也就可以跑起来了。

补:MD5函数

from hashlib import md5
def make_md5(s, encoding='utf-8'):
    return md5(s.encode(encoding)).hexdigest()

补:代码链接

很多人求源码,源码早已上传,链接没放在这里而已。

古人有言,书籍若非借阅,难以深入研读。对于公开的源代码,众人往往束之高阁,不加以细心琢磨,如此一来,对自己并无裨益。因此,在此处特意将其改为收费版本,以便有心之人能够获得。

代码链接地址:

未完待续…

二维码
扫一扫在手机端查看

本文链接:https://by928.com/9195.html     转载请注明出处和本文链接!请遵守 《网站协议》
我们凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求,请立即点击咨询我们或拨打咨询热线: 13761152229,我们会详细为你一一解答你心中的疑难。

项目经理在线

我们已经准备好了,你呢?

2020我们与您携手共赢,为您的企业形象保驾护航!

在线客服
联系方式

热线电话

13761152229

上班时间

周一到周五

公司电话

二维码
微信
线