自动化测试--基于python2.7+selenium+unittest+ddt+yaml搭建框架

目录

  • 一、框架结构目录
  • 二、读取文件
  • 三、配置文件config.py
  • 四、日志
  • 五、封装浏览器
  • 六、封装数据库
  • 七、selenium api二次封装
  • 七、测试报告
  • 八、封装邮件
  • 九、新建测试用例
  • 十、调用测试用例主程序
  • 十一、执行测试

前言:selenium api只封装部分,业务公共页面page、连接数据库封装,后续实践后补上。有问题欢迎留言补充,谢谢~

一、框架结构目录

自动化测试--基于python2.7+selenium+unittest+ddt+yaml搭建框架_第1张图片

二、读取文件

utils下新建file_reader.py

# -*- coding: utf-8 -*-
import yaml
import os
from xlrd import open_workbook

class YamlReader:
    def __init__(self, yamlf):
        if os.path.exists(yamlf):
            self.yamlf = yamlf
        else:
            raise FileNotFoundError('文件不存在!')
        self._data = None

    @property
    def data(self):
        # 如果是第一次调用data,读取yaml文档,否则直接返回之前保存的数据
        if not self._data:
            with open(self.yamlf, 'rb') as f:
                self._data = list(yaml.safe_load_all(f))  # load后是个generator,用list组织成列表
        return self._data
        
class SheetTypeError(Exception):
    pass

# 读取excel文件中的内容。返回list。
class ExcelReader:
    """
    如:
    excel中内容为:
    | A  | B  | C  |
    | A1 | B1 | C1 |
    | A2 | B2 | C2 |

    如果 print(ExcelReader(excel, title_line=True).data),输出结果:
    [{A: A1, B: B1, C:C1}, {A:A2, B:B2, C:C2}]

    如果 print(ExcelReader(excel, title_line=False).data),输出结果:
    [[A,B,C], [A1,B1,C1], [A2,B2,C2]]

    可以指定sheet,通过index或者name:
    ExcelReader(excel, sheet=2)
    ExcelReader(excel, sheet='BaiDuTest')
    """
    def __init__(self, excelpath, sheet=0, title_line=True):
        if os.path.exists(excelpath):
            self.excelpath = excelpath  # excel文件路径
        else:
            raise FileNotFoundError('文件不存在!')
        self.sheet = sheet   # sheet可以是int表示表格的索引,可以是str表示表格的名称
        self.title_line = title_line  # 是否存在标题行,有标题行,每一行都是都是对应列名的取值;没有标题行,每一行都是一个列表
        self._data = list()   # 用于存储每行生成的数据。

    @property
    def data(self):
        if not self._data:
            workbook = open_workbook(self.excelpath)
            if type(self.sheet) not in [int, str]:
                raise SheetTypeError('Please pass in  or , not {0}'.format(type(self.sheet)))
            elif type(self.sheet) == int:
                s = workbook.sheet_by_index(self.sheet)
            else:
                s = workbook.sheet_by_name(self.sheet)

            if self.title_line:
                title = s.row_values(0)  # 首行为title
                for col in range(1, s.nrows):
                    # 依次遍历其余行,与首行组成dict,拼到self._data中
                    self._data.append(dict(zip(title, s.row_values(col))))
            else:
                for col in range(0, s.nrows):
                    # 遍历所有行,拼到self._data中
                    self._data.append(s.row_values(col))
        return self._data

if __name__ == '__main__':
    y = 'D:\myProject\config\config.yml'
    reader = YamlReader(y)
    print(reader.data)
    
    e = 'D:/myProject/data/baidu.xlsx'
    reader = ExcelReader(e, title_line=True)
    print(reader.data)

三、配置文件config.py

utils下新建config.py

#-*- coding: utf-8 -*-
import os
from utils.file_reader import YamlReader

# 通过当前文件的绝对路径,其父级目录一定是框架的base目录,然后确定各层的绝对路径。如果你的结构不同,可自行修改。
# 之前直接拼接的路径,修改了一下,用现在下面这种方法,可以支持linux和windows等不同的平台,也建议大家多用os.path.split()和os.path.join(),不要直接+'\\xxx\\ss'这样
BASE_PATH = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0]
CONFIG_FILE = os.path.join(BASE_PATH, 'config', 'config.yml')
DATA_PATH = os.path.join(BASE_PATH, 'data')
DRIVER_PATH = os.path.join(BASE_PATH, 'drivers')
LOG_PATH = os.path.join(BASE_PATH, 'log')
REPORT_PATH = os.path.join(BASE_PATH, 'report')

class Config:
    def __init__(self, config=CONFIG_FILE):
        self.config = YamlReader(config).data

    def get(self, element, index=0):
        """
        yaml是可以通过'---'分节的。用YamlReader读取返回的是一个list,第一项是默认的节,如果有多个节,可以传入index来获取。
        这样我们其实可以把框架相关的配置放在默认节,其他的关于项目的配置放在其他节中。可以在框架中实现多个项目的测试。
        """
        return self.config[index].get(element)

config目录下新建config.yml

URL: http://www.baidu.com    # 测试网址
emailserver: smtp.qq.com    #发件箱邮件服务
from_user: [email protected]
from_passwd: tkmyrvvwigmwbihe
to_user: [email protected]
#mysql
mysql:
    ip: '192.168.3.6'
    user: 'root'
    pwd: 'password'
    db: 'mysql'
#oracle
oracle:
    ip: '192.168.3.1'
    user: 'root'
    pwd: 'password'
    port: '1521'
    sid: 'barc'

log:
    file_name: testcase.log      # 输出日志文件名
    backup: 5                # 备份名
    console_level: WARNING  # 控制台输出等级
    file_level: DEBUG       # 文件输出等级
    pattern: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'  # 打印输出格式

四、日志

utils下新建logger.py

# -*- coding: utf-8 -*-
import logging
import os
import unittest
import time
import os.path

class Logger(object):
    def __init__(self, logger, CmdLevel=logging.INFO, FileLevel=logging.INFO):
        self.logger = logging.getLogger(logger)
        self.logger.setLevel(logging.DEBUG)  # 设置日志默认级别为DEBUG
        fmt = logging.Formatter('%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s - %(message)s')  # 设置日志输出格式
        currTime = time.strftime('%Y%m%d%H%M', time.localtime(time.time()))  # 格式化当前时间
        log_path = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0]+'/log/'
        print('得到的日志路径为:', log_path)
        log_name = log_path + currTime + '.log'  # 设置日志文件名称

        # 设置由文件输出
        fh = logging.FileHandler(log_name, encoding='utf-8')  # 采用utf-8字符集格式防止出现中文乱码
        fh.setFormatter(fmt)
        fh.setLevel(FileLevel)  # 日志级别为INFO

        # 设置日志由控制台输出
        # sh = logging.StreamHandler(log_name)
        # sh.setFormatter(fmt)
        # sh.setLevel(CmdLevel)
        self.logger.addHandler(fh)  # 添加handler
        
    def getlog(self):
        return self.logger

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

五、封装浏览器

utils下新建browserengine.py

# -*- coding: utf-8 -*-
from selenium import webdriver
import configparser
import sys, os

class BrowserEngine(object):
    def __init__(self, driver):
        self.driver = driver
    browser_type = "Chrome"  
    def get_browser(self):
        if self.browser_type == 'Firefox':
            driver = webdriver.Firefox()
        elif self.browser_type == 'Chrome':
            driver = webdriver.Chrome()
        elif self.browser_type == 'IE':
            driver = webdriver.Ie()
        else:
            driver = webdriver.Chrome()
            
        driver.maximize_window()
        driver.implicitly_wait(10)

        return driver

六、封装数据库

utils下新建conndb.py

#-*- coding: utf-8 -*-
import cx_Oracle,pymysql,json,os
from utils.config import Config
os.environ['NLS_LANG'] = 'SIMPLIFIED CHINESE_CHINA.UTF8'

myora = Config().get('oracle')
mysql = Config().get('mysql')

class TestDb(object):
    def __init__(self,oracle_type):
        if oracle_type == 'oracle':
            self.connect = cx_Oracle.connect(myora['user'] + "/" + myora['pwd'] + "@" + myora['ip'] + ":" + myora['port'] + "/" + myora['sid'])
            self.cursor = self.connect.cursor()
        elif oracle_type == 'mysql':
            self.connect = pymysql.connect(mysql['ip'], mysql['user'], mysql['pwd'], mysql['db'])
            self.cursor = self.connect.cursor()
        else:
            print("请连接正确的数据库")

    def select(self,sql):
        try:
            self.cursor.execute(sql)
            result = self.cursor.fetchall()
            print('查询数据成功')
            return result
        except Exception as e:
            print(e)
        finally:
            self.disconnect()

        #返回json串脚本
        # list = []
        # self.cursor.execute(sql)
        # result = self.cursor.fetchall()
        # col_name = self.cursor.description
        # for row in result:
        #     dict = {}
        #     for col in range(len(col_name)):
        #         key = col_name[col][0]
        #         value = row[col]
        #         dict[key] = value
        #     list.append(dict)
        # js = json.dumps(list, ensure_ascii=False, indent=2, separators=(',', ':'))
        # return js

    def insert(self, sql, list_param):
        try:
            self.cursor.executemany(sql, list_param)
            self.connect.commit()
            print("插入ok")
        except Exception as e:
            print(e)
        finally:
            self.disconnect()

    def update(self, sql):
        try:
            self.cursor.execute(sql)
            self.connect.commit()

        except Exception as e:
            print(e)
        finally:
            self.disconnect()

    def delete(self, sql):
        try:
            self.cursor.execute(sql)
            self.connect.commit()
            print("delete ok")
        except Exception as e:
            print(e)
        finally:
            self.disconnect()

    def disconnect(self):
        self.cursor.close()
        self.connect.close()


if __name__ == "__main__":
    oracle = TestDb('oracle')
    A = oracle.select("SELECT T.CLASS_NAME FROM table001 T WHERE T.CLASS_ID='1'")
    print A[0][0]
    mysql = TestDb('mysql')
    B = mysql.select("select name FROM mysql.nametab")
    print B[0][0]

#     param = [('ww1', 'job003', 1333, 2), ('ss1', 'job004', 1444, 2)]
#     # test_oracle.insert("insert into bonus(ENAME,JOB,SAL,COMM)values(:1,:2,:3,:4)",param)#也可以下面这样解决orc-1036非法变量问题
#     test_oracle.insert("insert into bonus(ENAME,JOB,SAL,COMM)values (:ENAME,:JOB,:SAL,:COMM)", param)

七、selenium api二次封装

utils下新建basepage.py

# -*- coding:utf-8 -*-
import time
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.action_chains import ActionChains
import os.path
from utils.logger import Logger
from selenium.webdriver.support.select import Select
from selenium.webdriver.common.keys import Keys
import sys
reload(sys)
sys.setdefaultencoding('utf8')

logger = Logger("BasePage").getlog()

class BasePage(object):
    #"定义一个页面基类,让所有页面都继承这个类,封装一些常用的页面操作方法到这个类"

    def __init__(self, driver):
        self.driver = driver
        
    #8大页面元素(对象)定位方法的封装
    def find_element(self, selector):

        element = ''
        # if '=>' not in selector:
        #     return self.driver.find_element_by_id(selector)
        selector_by = selector[0]  
        selector_value = selector[1]  
        if selector_by == 'i' or selector_by == 'id':
            try:
                element = self.driver.find_element_by_id(selector_value)
                logger.info("定位元素OK,实际定位元素方法:%s ,定位的元素的属性值:%s" % (selector_by, selector_value))
            except NoSuchElementException as e:
                logger.error("没找到元素,抛出异常:%s" % e)
                self.get_window_img()  # 截取当前窗口
        elif selector_by == 'n' or selector_by == 'name':
            element = self.driver.find_element_by_name(selector_value)
        elif selector_by == 'c' or selector_by == 'class_name':
            element = self.driver.find_element_by_class_name(selector_value)
        elif selector_by == 'l' or selector_by == 'link_text':
            element = self.driver.find_element_by_link_text(selector_value)
        elif selector_by == 'p' or selector_by == 'partial_link_text':
            element = self.driver.find_element_by_partial_link_text(selector_value)
        elif selector_by == 't' or selector_by == 'tag_name':
            element = self.driver.find_element_by_tag_name(selector_value)
        elif selector_by == 'x' or selector_by == 'xpath':
            try:
                element = self.driver.find_element_by_xpath(selector_value)
                logger.info("定位元素OK,实际定位元素方法:%s ,定位的元素的属性值:%s" % (selector_by, selector_value))
            except NoSuchElementException as e:
                logger.error("没找到元素,抛出异常:%s" % e)
                self.get_window_img()  # 截取当前窗口

        elif selector_by == 'c' or selector_by == 'css_selector':
            element = self.driver.find_element_by_css_selector(selector_value)
        else:
            raise NameError("请输入正确的目标元素类型.")
        return element  # 返回变量element

    #输入框
    def input(self, selector, text):
        el = self.find_element(selector)
        el.clear()
        try:
            el.send_keys(text)
            logger.info("输入的文本内容为:%s" % text)
        except NameError as e:
            logger.error("输入的内容异常,抛出异常:%s" % e)
            self.get_window_img()

    #清除文本框内容
    def clear(self, selector):
        el = self.find_element(selector)
        try:
            el.clear()
            logger.info("清除输入框文本信息OK")
        except NameError as e:
            logger.error("清除输入框内容失败:抛出异常: %s" % e)
            self.get_window_img()
            
    @staticmethod
    def wait(seconds):
        time.sleep(seconds)
        logger.info("Sleep for %d seconds" % seconds)

    # 点击
    def click(self, selector):
        el = self.find_element(selector)
        try:
            el.click()
            logger.info("点击元素完成")
        except NameError as e:
            logger.error("没有找到元素 %s" % e)

    # 切到iframe
    def switch_frame(self):
        iframe = self.find_element(['classname','embed-responsive-item'])
        try:
            self.driver.switch_to_frame(iframe)
            # logger.info("The element \' %s \' was clicked." % iframe.text)
        except NameError as e:
            logger.error("Failed to click the element with %s" % e)

    # 处理标准下拉选择框,随机选择
    def select(self, id):
        select1 = self.find_element(id)
        try:
            options_list = select1.find_elements_by_tag_name('option')
            del options_list[0]
            s1 = choice(options_list)
            Select(select1).select_by_visible_text(s1.text)
            logger.info("随机选的是:%s" % s1.text)
        except NameError as e:
            logger.error("Failed to click the element with %s" % e)

    # 模拟回车键
    def enter(self, selector):
        e1 = self.find_element(selector)
        e1.send_keys(Keys.ENTER)

    def get_alert(self):
        el = self.driver.switch_to.alert  # 获取窗口弹窗的方法
        try:
            assert '用户名或者密码错误' in el.text  # el.text方法获取提示框内容
            logger.info("弹窗提示正确")
            el.accept()  # 点击弹窗确认按钮
        except Exception as e:
            print('弹窗提示错误', format(e))

    # 截图功能:得到截图并保存图片到项目image目录下
    def get_window_img(self):
        file_path = os.path.dirname(os.path.abspath('.')) + '/image/'  # 设置存放截图的路径
        # print('截图保存路径为:%s' % file_path)
        timeset = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))  # 格式化时间
        pic_name = file_path + timeset + '.png'  # 定义截图文件名称
        try:
            self.driver.get_screenshot_as_file(pic_name)
            logger.info('截图成功,图片保存路径为:/image.')
        except Exception as e:
            logger.error('截图出现异常', format(e))
            self.get_window_img()

    #隐士等待
    def sleep(self, seconds):
        self.driver.implicitly_wait(seconds)
        logger.info("设置隐式时间:%d 秒." % seconds)

    # 关闭当前窗口
    def close_window(self):
        try:
            self.driver.close()
            logger.info("关闭当前窗口.")
        except NameError as e:
            logger.error("关闭当前窗口出错,抛出错误提示:%s." % e)

    #进入iframe框架
    def switch_to_frame(self, selector):
        el = self.find_element(selector)
        self.driver.switch_to.frame(el)

    #切除iframe框架
    def switch_to_default(self):
        self.driver.switch_to.default_content()

    #执行JS脚本
    def execute_script(self, script):
        self.driver.execute_script(script)

    #右键
    def right_click(self, selector):
        el = self.find_element(selector)
        ActionChains(self.driver).context_click(el).perform()

    #上传文件
    def upload_input(self, selector, file):
        self.find_element(selector).send_keys(file)

    def upload_not_input(self, file):
        '''
        上传文件 ( 标签不是 input 类型,使用 win32gui,得先安装 pywin32 依赖包)
                                                pip install pywin32
        :param browser_type: 浏览器类型(Chrome浏览器和Firefox浏览器的有区别)
        :param file: 将要上传的文件(绝对路径)
        单个文件:file1 = 'C:\\Users\\list_tuple_dict_test.py'
        同时上传多个文件:file2 = '"C:\\Users\\list_tuple_dict_test.py" "C:\\Users\\class_def.py"'
        :return: 无
        '''
        # Chrome 浏览器是'打开'
        # 对话框
        # 下载个 Spy++ 工具,定位“打开”窗口,定位到窗口的类(L):#32770, '打开'为窗口标题
        dialog = win32gui.FindWindow('#32770', u'打开')
        ComboBoxEx32 = win32gui.FindWindowEx(dialog, 0, 'ComboBoxEx32', None)
        ComboBox = win32gui.FindWindowEx(ComboBoxEx32, 0, 'ComboBox', None)
        # 上面三句依次寻找对象,直到找到输入框Edit对象的句柄
        Edit = win32gui.FindWindowEx(ComboBox, 0, 'Edit', None)
        # 确定按钮Button
        button = win32gui.FindWindowEx(dialog, 0, 'Button', None)
        # 往输入框输入绝对地址
        win32gui.SendMessage(Edit, win32con.WM_SETTEXT, 0, file)
        # 按button
        win32gui.SendMessage(dialog, win32con.WM_COMMAND, 1, button)

七、测试报告

utils下新建HTMLTestRunner.py

#-*- coding: utf-8 -*-
"""
A TestRunner for use with the Python unit testing framework. It
generates a HTML report to show the result at a glance.

The simplest way to use this is to invoke its main method. E.g.

    import unittest
    import HTMLTestRunner

    ... define your tests ...

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


For more customization options, instantiates a HTMLTestRunner object.
HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.

    # output to a file
    fp = file('my_report.html', 'wb')
    runner = HTMLTestRunner.HTMLTestRunner(
                stream=fp,
                title='My unit testcase',
                description='This demonstrates the report output by HTMLTestRunner.'
                )

    # Use an external stylesheet.
    # See the Template_mixin class for more customizable options
    runner.STYLESHEET_TMPL = ''

    # run the testcase
    runner.run(my_test_suite)


------------------------------------------------------------------------
Copyright (c) 2004-2007, Wai Yip Tung
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.
* Neither the name Wai Yip Tung nor the names of its contributors may be
  used to endorse or promote products derived from this software without
  specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

# URL: http://tungwaiyip.info/software/HTMLTestRunner.html

__author__ = "Wai Yip Tung"
__version__ = "0.8.2"


"""
Change History

Version 0.8.2
* Show output inline instead of popup window (Viorel Lupu).

Version in 0.8.1
* Validated XHTML (Wolfgang Borgert).
* Added description of testcase classes and testcase cases.

Version in 0.8.0
* Define Template_mixin class for customization.
* Workaround a IE 6 bug that it does not treat 

%(heading)s
%(report)s
%(ending)s



"""
    # variables: (title, generator, stylesheet, heading, report, ending)


    # ------------------------------------------------------------------------
    # Stylesheet
    #
    # alternatively use a  for external style sheet, e.g.
    #   

    STYLESHEET_TMPL = """

"""



    # ------------------------------------------------------------------------
    # Heading
    #

    HEADING_TMPL = """

%(title)s

%(parameters)s

%(description)s

"""
# variables: (title, parameters, description) HEADING_ATTRIBUTE_TMPL = """

%(name)s: %(value)s

"""
# variables: (name, value) # ------------------------------------------------------------------------ # Report # REPORT_TMPL = """

Show Summary Failed All

%(test_list)s
Test Group/Test case Count Pass Fail Error View
Total %(count)s %(Pass)s %(fail)s %(error)s  
"""
# variables: (test_list, count, Pass, fail, error) REPORT_CLASS_TMPL = r""" %(desc)s %(count)s %(Pass)s %(fail)s %(error)s Detail """ # variables: (style, desc, count, Pass, fail, error, cid) REPORT_TEST_WITH_OUTPUT_TMPL = r"""
%(desc)s
%(status)s """
# variables: (tid, Class, style, desc, status) REPORT_TEST_NO_OUTPUT_TMPL = r"""
%(desc)s
%(status)s """
# variables: (tid, Class, style, desc, status) REPORT_TEST_OUTPUT_TMPL = r""" %(id)s: %(output)s """ # variables: (id, output) # ------------------------------------------------------------------------ # ENDING # ENDING_TMPL = """
 
"""
# -------------------- The end of the Template class ------------------- TestResult = unittest.TestResult class _TestResult(TestResult): # note: _TestResult is a pure representation of results. # It lacks the output and reporting ability compares to unittest._TextTestResult. def __init__(self, verbosity=1): TestResult.__init__(self) self.stdout0 = None self.stderr0 = None self.success_count = 0 self.failure_count = 0 self.error_count = 0 self.verbosity = verbosity # result is a list of result in 4 tuple # ( # result code (0: success; 1: fail; 2: error), # TestCase object, # Test output (byte string), # stack trace, # ) self.result = [] def startTest(self, test): TestResult.startTest(self, test) # just one buffer for both stdout and stderr self.outputBuffer = StringIO.StringIO() stdout_redirector.fp = self.outputBuffer stderr_redirector.fp = self.outputBuffer self.stdout0 = sys.stdout self.stderr0 = sys.stderr sys.stdout = stdout_redirector sys.stderr = stderr_redirector def complete_output(self): """ Disconnect output redirection and return buffer. Safe to call multiple times. """ if self.stdout0: sys.stdout = self.stdout0 sys.stderr = self.stderr0 self.stdout0 = None self.stderr0 = None return self.outputBuffer.getvalue() def stopTest(self, test): # Usually one of addSuccess, addError or addFailure would have been called. # But there are some path in unittest that would bypass this. # We must disconnect stdout in stopTest(), which is guaranteed to be called. self.complete_output() def addSuccess(self, test): self.success_count += 1 TestResult.addSuccess(self, test) output = self.complete_output() self.result.append((0, test, output, '')) if self.verbosity > 1: sys.stderr.write('ok ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('.') def addError(self, test, err): self.error_count += 1 TestResult.addError(self, test, err) _, _exc_str = self.errors[-1] output = self.complete_output() self.result.append((2, test, output, _exc_str)) if self.verbosity > 1: sys.stderr.write('E ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('E') def addFailure(self, test, err): self.failure_count += 1 TestResult.addFailure(self, test, err) _, _exc_str = self.failures[-1] output = self.complete_output() self.result.append((1, test, output, _exc_str)) if self.verbosity > 1: sys.stderr.write('F ') sys.stderr.write(str(test)) sys.stderr.write('\n') else: sys.stderr.write('F') class HTMLTestRunner(Template_mixin): """ """ def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None,tester=None): self.stream = stream self.verbosity = verbosity if title is None: self.title = self.DEFAULT_TITLE else: self.title = title if description is None: self.description = self.DEFAULT_DESCRIPTION else: self.description = description if tester is None: self.description = self.DEFAULT_DESCRIPTION else: self.description = description self.startTime = datetime.datetime.now() def run(self, test): "Run the given testcase case or testcase suite." result = _TestResult(self.verbosity) test(result) self.stopTime = datetime.datetime.now() self.generateReport(test, result) print >>sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime) return result def sortResult(self, result_list): # unittest does not seems to run in any particular order. # Here at least we want to group them together by class. rmap = {} classes = [] for n,t,o,e in result_list: cls = t.__class__ if not rmap.has_key(cls): rmap[cls] = [] classes.append(cls) rmap[cls].append((n,t,o,e)) r = [(cls, rmap[cls]) for cls in classes] return r def getReportAttributes(self, result): """ Return report attributes as a list of (name, value). Override this to add custom attributes. """ startTime = str(self.startTime)[:19] duration = str(self.stopTime - self.startTime) status = [] if result.success_count: status.append('Pass %s' % result.success_count) if result.failure_count: status.append('Failure %s' % result.failure_count) if result.error_count: status.append('Error %s' % result.error_count ) if status: status = ' '.join(status) else: status = 'none' return [ ('Start Time', startTime), ('Duration', duration), ('Status', status), ] def generateReport(self, test, result): report_attrs = self.getReportAttributes(result) generator = 'HTMLTestRunner %s' % __version__ stylesheet = self._generate_stylesheet() heading = self._generate_heading(report_attrs) report = self._generate_report(result) ending = self._generate_ending() output = self.HTML_TMPL % dict( title = saxutils.escape(self.title), generator = generator, stylesheet = stylesheet, heading = heading, report = report, ending = ending, ) self.stream.write(output.encode('utf8')) def _generate_stylesheet(self): return self.STYLESHEET_TMPL def _generate_heading(self, report_attrs): a_lines = [] for name, value in report_attrs: line = self.HEADING_ATTRIBUTE_TMPL % dict( name = saxutils.escape(name), value = saxutils.escape(value), ) a_lines.append(line) heading = self.HEADING_TMPL % dict( title = saxutils.escape(self.title), parameters = ''.join(a_lines), description = saxutils.escape(self.description), ) return heading def _generate_report(self, result): rows = [] sortedResult = self.sortResult(result.result) for cid, (cls, cls_results) in enumerate(sortedResult): # subtotal for a class np = nf = ne = 0 for n,t,o,e in cls_results: if n == 0: np += 1 elif n == 1: nf += 1 else: ne += 1 # format class description if cls.__module__ == "__main__": name = cls.__name__ else: name = "%s.%s" % (cls.__module__, cls.__name__) doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" desc = doc and '%s: %s' % (name, doc) or name row = self.REPORT_CLASS_TMPL % dict( style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', desc = desc, count = np+nf+ne, Pass = np, fail = nf, error = ne, cid = 'c%s' % (cid+1), ) rows.append(row) for tid, (n,t,o,e) in enumerate(cls_results): self._generate_report_test(rows, cid, tid, n, t, o, e) report = self.REPORT_TMPL % dict( test_list = ''.join(rows), count = str(result.success_count+result.failure_count+result.error_count), Pass = str(result.success_count), fail = str(result.failure_count), error = str(result.error_count), ) return report def _generate_report_test(self, rows, cid, tid, n, t, o, e): # e.g. 'pt1.1', 'ft1.1', etc has_output = bool(o or e) tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1) name = t.id().split('.')[-1] doc = t.shortDescription() or "" desc = doc and ('%s: %s' % (name, doc)) or name tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL # o and e should be byte string because they are collected from stdout and stderr? if isinstance(o,str): # TODO: some problem with 'string_escape': it escape \n and mess up formating # uo = unicode(o.encode('string_escape')) uo = o.decode('latin-1') else: uo = o if isinstance(e,str): # TODO: some problem with 'string_escape': it escape \n and mess up formating # ue = unicode(e.encode('string_escape')) ue = e.decode('latin-1') else: ue = e script = self.REPORT_TEST_OUTPUT_TMPL % dict( id = tid, output = saxutils.escape(uo+ue), ) row = tmpl % dict( tid = tid, Class = (n == 0 and 'hiddenRow' or 'none'), style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'), desc = desc, script = script, status = self.STATUS[n], ) rows.append(row) if not has_output: return def _generate_ending(self): return self.ENDING_TMPL ############################################################################## # Facilities for running tests from the command line ############################################################################## # Note: Reuse unittest.TestProgram to launch testcase. In the future we may # build our own launcher to support more specific command line # parameters like testcase title, CSS, etc. class TestProgram(unittest.TestProgram): """ A variation of the unittest.TestProgram. Please refer to the base class for command line parameters. """ def runTests(self): # Pick HTMLTestRunner as the default testcase runner. # base class's testRunner parameter is not useful because it means # we have to instantiate HTMLTestRunner before we know self.verbosity. if self.testRunner is None: self.testRunner = HTMLTestRunner(verbosity=self.verbosity) unittest.TestProgram.runTests(self) main = TestProgram ############################################################################## # Executing this module from the command line ############################################################################## if __name__ == "__main__": main(module=None)

八、封装邮件

utils下新建send_email.py

#-*- coding: utf-8 -*-
"""
邮件类。用来给指定用户发送邮件。可指定多个收件人,可带附件。
"""
import re
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from socket import gaierror, error
from utils.logger import Logger
from utils.config import REPORT_PATH

class Email:
    def __init__(self, server, sender, password, receiver, title, message=None, path=None):
        """初始化Email

        :param title: 邮件标题,必填。
        :param message: 邮件正文,非必填。
        :param path: 附件路径,可传入list(多附件)或str(单个附件),非必填。
        :param server: smtp服务器,必填。
        :param sender: 发件人,必填。
        :param password: 发件人密码,必填。
        :param receiver: 收件人,多收件人用“;”隔开,必填。
        """
        self.title = title
        self.message = message
        self.files = path

        self.msg = MIMEMultipart('related')

        self.server = server
        self.sender = sender
        self.receiver = receiver
        self.password = password

    def _attach_file(self, att_file):
        """将单个文件添加到附件列表中"""
        att = MIMEText(open('%s' % att_file, 'rb').read(), 'plain', 'utf-8')
        att["Content-Type"] = 'application/octet-stream'
        file_name = re.split(r'[\\|/]', att_file)
        att["Content-Disposition"] = 'attachment; filename="%s"' % file_name[-1]
        self.msg.attach(att)
        logger.info('attach file {}'.format(att_file))

    def send(self):
        self.msg['Subject'] = self.title
        self.msg['From'] = self.sender
        self.msg['To'] = self.receiver

        # 邮件正文
        if self.message:
            self.msg.attach(MIMEText(self.message))

        # 添加附件,支持多个附件(传入list),或者单个附件(传入str)
        if self.files:
            if isinstance(self.files, list):
                for f in self.files:
                    self._attach_file(f)
            elif isinstance(self.files, str):
                self._attach_file(self.files)

        # 连接服务器并发送
        try:
            smtp_server = smtplib.SMTP(self.server)  # 连接sever
        except (gaierror and error) as e:
            logger.exception('发送邮件失败,无法连接到SMTP服务器,检查网络以及SMTP服务器. %s', e)
        else:
            try:
                smtp_server.login(self.sender, self.password)  # 登录
            except smtplib.SMTPAuthenticationError as e:
                logger.exception('用户名密码验证失败!%s', e)
            else:
                smtp_server.sendmail(self.sender, self.receiver.split(';'), self.msg.as_string())  # 发送邮件
            finally:
                smtp_server.quit()  # 断开连接
                logger.info('发送邮件"{0}"成功! 收件人:{1}。如果没有收到邮件,请检查垃圾箱,'
                            '同时检查收件人地址是否正确'.format(self.title, self.receiver))

九、新建测试用例

testcase下新建test_baidu.py

# -*- coding:utf-8 -*-
import time
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from utils.browserengine import BrowserEngine
from utils.config import Config,DATA_PATH
from utils.basepage import BasePage
import ddt
from utils.file_reader import ExcelReader

@ddt.ddt
class TestBaiDu(unittest.TestCase):
    URL = Config().get('URL')
    excel = DATA_PATH + '/baidu.xlsx'

    def setUp(self):
        browserengine = BrowserEngine(self)
        self.driver = browserengine.get_browser()
        self.driver.get(self.URL)

    @ddt.data(*ExcelReader(excel).data)
    def test_search_0(self,data):
        test = BasePage(self.driver)
        test.input(['i','kw'],data['search'])
        time.sleep(3)

    def tearDown(self):
        self.driver.quit()


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

十、调用测试用例主程序

myProject下新建run_all_case.py

#-*- coding: utf-8 -*-
#import HTMLTestRunner  # 导入开源的测试报告生成HTML格式的模块
import os.path
import time
import unittest
from utils.config import Config
from utils.send_email import Email
from email.mime.text import MIMEText
from email.header import Header
import smtplib
from utils.HTMLTestRunner import HTMLTestRunner


case_path = os.path.join(os.getcwd(),"testcase")
print case_path
report_path = os.path.join(os.getcwd(),"report")
print report_path

# ===============定义邮件发送============
def send_mail(file_new):
    # config = configparser.ConfigParser()
    # file_path = os.path.dirname(os.path.abspath('.')) + '/V2200/config/config.ini'
    # config.read(file_path, encoding='UTF-8')  # 读取config配置文件
    emailserver = Config().get('emailserver')
    msg_from = Config().get('from_user')
    passwd = Config().get('from_passwd')
    msg_to = Config().get('to_user')
    f = open(file_new, 'rb')
    mail_boy = f.read()
    f.close()
    msg = MIMEText(mail_boy, 'html', 'utf-8')  # 定义邮件正文
    msg['Subject'] = Header('myProject自动化测试报告', 'utf-8')
    msg['From'] = msg_from
    msg['To'] = msg_to
    # 定义邮件标题
    smtp = smtplib.SMTP_SSL(emailserver, 465)  # 邮件服务器及端口号
    smtp.login(msg_from, passwd)  # 登录SMTP服务器
    smtp.sendmail(msg_from, msg_to, msg.as_string())  # 发邮件 as_string()把MIMEText对象变成str
    smtp.quit()
    print("邮件发送成功,请注意查收!")

# ==============测试报告文件===========
def new_report(report_path):
    lists = os.listdir(report_path)  # 得到项目目录下所有的文件和文件夹
    lists.sort(key=lambda fn: os.path.getmtime(report_path + '\\' + fn))  # 将得到的文件和文件夹按创建时间排序
    file_new = os.path.join(report_path, lists[-1])  # 获取最新创建的文件
    print(file_new)
    return file_new

def all_case():
    case = unittest.defaultTestLoader.discover(case_path, pattern="test_baidu.py", top_level_dir=None)
    print(case)
    return case

if __name__ == '__main__':
    # 获取当前时间,并格式化时间
    now_time = time.strftime("%Y-%m-%d-%H_%M_%S", time.localtime(time.time()))
    # html测试报告路径
    report_html = os.path.join(report_path, "Result_" + now_time + ".html")
    fp = open(report_html, 'wb')  # 打开一个文件,将测试结果写入该文件中

    runner = HTMLTestRunner(stream=fp,
                            title=u'myProject自动化测试报告,测试结果如下:',
                            description=u'用例执行情况:')

    runner.run(all_case())  # 执行所有测试case
    fp.close()
    mail_report = new_report(report_path)
    send_mail(mail_report)

十一、执行测试

运行run_all_case.py前,data下需新建baidu.xlsx,文本内容参考如下

search
selenium
中文

第一行为字段名, 需与test_baidu.py中的search对应

你可能感兴趣的:(自动化测试)