unittest单元测试框架基础知识

 一 、unittest模块的属性介绍

UnitTest:
  在Python自动化领域有两个绕不开的测试框架,分别是UnitTest和PyTest
  UnitTest本身是单元测试框架,截止到目前,已经可以基于此框架来实现Selenium、Appium、Requests接口自动化
  UnitTest已经默认安装在python环境中了,不需要再额外进行安装
四大特性:
  1.前置与后置,Setup和Teardown
  2.测试用例,所有的用例都是基于TestCase类来实现的
  3.测试套件与运行器,可以精细化管理测试用例以及生成测试报告
  4.断言机制,封装了很多断言,可以直接通过self.来调用所有已封装好的断言函数

1.1 unittest的属性如下(红色部分为常用属性):

['BaseTestSuite', 'FunctionTestCase', 'SkipTest', 'TestCase', 'TestLoader', 'TestProgram', 'TestResult', 'TestSuite', 'TextTestResult', 'TextTestRunner',  'case', 'defaultTestLoader', 'expectedFailure', 'findTestCases', 'getTestCaseNames', 'installHandler', 'loader', 'main', 'makeSuite', 'registerResult', 'removeHandler', 'removeResult', 'result', 'runner', 'signals', 'skip', 'skipIf', 'skipUnless', 'suite', 'util']

1.2 常用属性介绍

unittest.TestCase:TestCase类,所有测试用例类继承的基本类,该类下所有以test开头的函数都是测试用例

class BaiduTest(unittest.TestCase):
  pass
  
   def test_case1(self):
     pass

unittest.main():使用她可以方便的将一个单元测试模块变为可直接运行的测试脚本,main()方法使用TestLoader类来搜索所有包含在该模块中以“test”命名开头的测试方法,并自动执行他们。执行方法的默认顺序是:根据ASCII码的顺序加载测试用例,数字与字母的顺序为:0-9,A-Z,a-z。所以以A开头的测试用例方法会优先执行,以a开头会后执行。

 

unittest.TestSuite():unittest框架的TestSuite()类是用来创建测试套件的,即多个测试用例的集合

unittest.TextTextRunner():unittest框架的TextTextRunner()类,通过该类下面的run()方法来运行suite所组装的测试用例,入参为suite测试套件。

 

unittest.defaultTestLoader(): defaultTestLoader()类,通过该类下面的discover()方法可自动根据测试目录start_dir匹配查找测试用例文件(test*.py),并将查找到的测试用例组装到测试套件,因此可以直接通过run()方法执行discover。用法如下:

discover=unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py')

 

unittest.skip():装饰器,当运行用例时,有些用例可能不想执行等,可用装饰器暂时屏蔽该条测试用例。一种常见的用法就是比如说想调试某一个测试用例,想先屏蔽其他用例就可以用装饰器屏蔽。

@unittest.skip(‘跳过原因’)装饰器:无条件跳过装饰的测试,并说明跳过测试的原因。

@unittest.skipIf('条件',‘跳过原因’)装饰器:条件为真时,跳过装饰的测试,并说明跳过测试的原因。

@unittest.skipUnless('条件',‘跳过原因’)装饰器:条件为假时,跳过装饰的测试,并说明跳过测试的原因。

@unittest.expectedFailure:预言会失败,先执行,如果失败不会报错,会忽略这个测试用例,和跳过的差别在于跳过不执行用例,这个是执行用例失败后跳过

二、各属性详细介绍

1.TestCase类的常用属性

setUp():setUp()方法用于测试用例执行前的初始化工作。如测试用例中需要访问数据库,可以在setUp中建立数据库连接并进行初始化。如测试用例需要登录web,可以先实例化浏览器。

tearDown():tearDown()方法用于测试用例执行之后的善后工作。如关闭数据库连接。关闭浏览器。

assert*():一些断言方法:在执行测试用例的过程中,最终用例是否执行通过,是通过判断测试得到的实际结果和预期结果是否相等决定的。

assertEqual(a,b,[msg='测试失败时打印的信息']):断言a和b是否相等,相等则测试用例通过。

assertNotEqual(a,b,[msg='测试失败时打印的信息']):断言a和b是否相等,不相等则测试用例通过。

assertTrue(x,[msg='测试失败时打印的信息']):断言x是否True,是True则测试用例通过。

assertFalse(x,[msg='测试失败时打印的信息']):断言x是否False,是False则测试用例通过。

assertIs(a,b,[msg='测试失败时打印的信息']):断言a是否是b,是则测试用例通过。

assertNotIs(a,b,[msg='测试失败时打印的信息']):断言a是否是b,不是则测试用例通过。

assertIsNone(x,[msg='测试失败时打印的信息']):断言x是否None,是None则测试用例通过。

assertIsNotNone(x,[msg='测试失败时打印的信息']):断言x是否None,不是None则测试用例通过。

assertIn(a,b,[msg='测试失败时打印的信息']):断言a是否在b中,在b中则测试用例通过。

assertNotIn(a,b,[msg='测试失败时打印的信息']):断言a是否在b中,不在b中则测试用例通过。

assertIsInstance(a,b,[msg='测试失败时打印的信息']):断言a是是b的一个实例,是则测试用例通过。

assertNotIsInstance(a,b,[msg='测试失败时打印的信息']):断言a是是b的一个实例,不是则测试用例通过

2.TestSuite类的常用属性(组织用例时需要用到,配合TextTextRunner

[ 'addTest', 'addTests', 'countTestCases', 'debug', 'run']

说明:

addTest(): addTest()方法是将测试用例添加到测试套件中,如下方,是将test_baidu模块下的BaiduTest类下的test_baidu测试用例添加到测试套件。

suite = unittest.TestSuite()
suite.addTest(test_baidu.BaiduTest('test_baidu'))

3.TextTextRunner的属性如下:(组织用例时需要用到,配合TestSuite

[' 'buffer', 'descriptions', 'failfast', 'resultclass', 'run', 'stream', 'verbosity']

说明:

run(): run()方法是运行测试套件的测试用例,入参为suite测试套件。

runner = unittest.TextTestRunner()
runner.run(suite)

 三、unittest基本使用

3.1 基本用法

以下以selenium操作谷歌浏览器打开网页为例,讲解unittest的用法

将selenium访问谷歌的线性代码简单封装得到web_key_demo逻辑代码,基于封装好的逻辑代码,可以引入unittest来管理多个测试用例

import time
from selenium import webdriver
"""
关键字驱动类:底层逻辑代码,这个类不会直接执行测试行为。
属于逻辑层代码,常规的操作行为封装成自定义的关键字函数。
在执行自动化的时候,通过调用自定义函数类,来实现自动化测试操作。
常规操作行为:
1.创建浏览器对象
2.访问url
3.元素定位
4.输入
5.点击
6.其他...
"""
#基于type生成对应的浏览器对象
def browser(type_):
    try:
        # driver本身只限于webdriver对象
        driver = getattr(webdriver,type_)()
    except Exception as e:
        driver = webdriver.Chrome()
    return driver

class KeyDemo:
    def __init__(self, type_):
        self.driver = browser(type_)
    #访问url
    def open(self,url):
        self.driver.get(url)
    #元素定位:8种元素定位
    def locator(self,name,value):
        return self.driver.find_element(name,value)
    #输入
    def input(self, name,value,txt):
        self.locator(name, value).send_keys(txt)
    #点击
    def click(self, name,value):
        self.locator(name,value).click()
    #关闭浏览器
    def quit(self):
        self.driver.quit()
    def sleep(self,time_):
        time.sleep(time_)
web_key_demo

使用unittest编写python的单元测试代码,包括如下几个步骤:

1、编写一个python类,继承 unittest模块中的TestCase类,这就是一个测试类

2、在上面编写的测试类中定义测试方法(这个就是指的测试用例),每个方法的方法名要求以 test 打头,没有额外的参数。 在该测试方法中 调用被测试代码,校验测试结果,TestCase类中提供了很多标准的校验方法,如 最常见的assertEqual。

3、执行 unittest.main() ,该函数会负责运行测试,它会实例化所有TestCase的子类,并运行其中所有以test打头的方法

import unittest # python自带,不需要额外安装
from web_key_demo import KeyDemo

class CaseDemo(unittest.TestCase):

    # 在每个测试用例执行前先执行
    def setUp(self) -> None:
        self.kd = KeyDemo('Chrome')

    # 每个测试用例执行后紧接着执行
    def tearDown(self) -> None:
        self.kd.quit()

    def test_01(self): # 测试用例名字需要以test开头
        self.kd.open(' http://www.baidu.com ')
        self.kd.input('id', 'kw', '美女')
        self.kd.click('id', 'su')
        self.kd.sleep(3)

    def test_02(self):
        self.kd = KeyDemo('Chrome')
        self.kd.open(' http://www.baidu.com ')
        self.kd.input('id', 'kw', '妹子')
        self.kd.click('id', 'su')
        self.kd.sleep(3)

    def test_03(self):
        pass

if __name__ == '__main__':
    # 执行该脚本执行所有的以test开头的测试用例,执行顺序为默认的顺序:
        # setUp -- test_01 -- tearDown
        # setUp -- test_02 -- tearDown
        # setUp -- test_03 -- tearDown
    unittest.main()

3.2 断言、跳过、预言失败

import unittest # python自带,不需要额外安装
from ddt import ddt,data,file_data,unpack # 需要 pip install ddt


@ddt # 装饰ddt来声明我要用这个ddt来管理测试类
class CaseDemo(unittest.TestCase):

    # 如果希望从外边向测试用例类传参,就需要调用父类的init,从源码可以看到父类只有methodName='runTest'这么一个参数
    def __init__(self, methodName='runTest',wechat=None):
        super().__init__(methodName=methodName)
        self.wechat = wechat

    # 在测试用例执行前先执行
    def setUp(self) -> None:
        # self.kd = KeyDemo('Chrome')
        # print(self.wechat)
        print('+++++++++++++++++++++++++++++++++++')
        pass
    # 测试用例执行后紧接着执行
    def tearDown(self) -> None:
        # self.kd.quit()
        print('------------------------------------')
        pass

    # ======================================================= 跳过 ====================================================
    @unittest.skip('我无条件跳过,就无条件跳过')
    def test_skip1(self):
        print('我还有机会BB么??')

    @unittest.skipUnless(1>2, '判断结果为False,我就跳过')
    def test_skip2(self):
        print('我还有机会BB么??')

    @unittest.skipIf(1<2, '判断结果为True,我就跳过')
    def test_skip3(self):
        print('我还有机会BB么??')

    # ======================================================= 预言失败 ================================================
    # 预言失败:先执行,如果失败不会报错,会忽略这个测试用例,和跳过的差别在于跳过不执行用例,这个是执行用例失败后跳过
    @unittest.expectedFailure
    def test_aa(self):
        pass
        self.assertEqual(123, 321, msg='果然失败了')

    # ======================================================= 断言 ====================================================
    def test_assert(self):
        """'断言测试,msg是断言失败后提示信息,可以不要'
        """
        def _test():
            return '老王'

        res = _test()
        # 各种断言的方法都是内置属性,可以选择合适的来用

        self.assertEqual(res,'老赵', msg=f'不是老赵干的别冤枉他,是{res}干的') # 断言value1 == value2

        # # 失败后返回 AssertionError: '老王' not found in ['老张', '老李'] : 老王不在嫌疑人里面里面,不是他
        # self.assertIn(res,['老张','老李'],msg=f'{res}不在嫌疑人里面里面,不是他')

        # # 失败后返回: '老王' is not false : 返回不是False
        # self.assertFalse(res,msg='返回不是False')



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

3.3 测试用例找不到报错

注意执行的时候光标不能放入CaseDemo类中,需要放到外面,点一下其他位置再执行,不然会报错

AttributeError: type object 'CaseDemo' has no attribute 'test_01'

Error
Traceback (most recent call last):
  File "E:\python3.8\lib\unittest\case.py", line 60, in testPartExecutor
    yield
  File "E:\python3.8\lib\unittest\case.py", line 676, in run
    self._callTestMethod(testMethod)
  File "E:\python3.8\lib\unittest\case.py", line 633, in _callTestMethod
    method()
  File "E:\python3.8\lib\unittest\loader.py", line 34, in testFailure
    raise self._exception
  File "E:\python3.8\lib\unittest\loader.py", line 169, in loadTestsFromName
    parent, obj = obj, getattr(obj, part)
AttributeError: type object 'CaseDemo' has no attribute 'test_01'
报错信息

四、unittest结合ddt模块 给测试用例传参

>>更多ddt详细用法点这里<<

unittest 和 ddt 有很好的契合度,ddt读取的数据可以直接用于unittest

4.1 ddt.data直接传参

import unittest # python自带,不需要额外安装
from ddt import ddt,data,file_data,unpack # 需要 pip install ddt
from web_key_demo import KeyDemo

@ddt # 装饰ddt来声明我要用这个ddt来管理测试类
class CaseDemo(unittest.TestCase):

    # 在测试用例执行前先执行
    def setUp(self) -> None:
        print('+++++++++++++++++++++++++++++++++++')
        pass
    # 测试用例执行后紧接着执行
    def tearDown(self) -> None:
        print('------------------------------------')
        pass

    # ======================================================= ddt的data直接传参 =======================================
    @data('隔壁老王') # 单个传参
    def test_01(self,name):
        print(name)

    @data(['隔壁老赵','嘿咻嘿咻'])  # 多个参数
    @unpack # 多个参数需要这样解压
    def test_02(self, name, action):
        print(name)
        print(action)

    @data(['隔壁老王', '睡觉'],['隔壁老李', '幽会'])  # 多组数据,每组多个参数,几组数据执行几次函数
    @unpack  # 需要这样解压
    def test_03(self, name, action):
        print(name)
        print(action)


if __name__ == '__main__':
    unittest.main()
    """
    +++++++++++++++++++++++++++++++++++
隔壁老王
------------------------------------
+++++++++++++++++++++++++++++++++++
隔壁老赵
嘿咻嘿咻
------------------------------------
+++++++++++++++++++++++++++++++++++
隔壁老王
睡觉
------------------------------------
+++++++++++++++++++++++++++++++++++
隔壁老李
幽会
------------------------------------


Ran 4 tests in 0.013s

OK
    """

4.2 ddt模块读取yaml文件

第三节代码可以发现,测试用例01和02只是数据不同,可以采取数据驱动

首先需要安装ddt和PyYAML模块,用ddt读取yaml数据文件中一组一组数据为一个个的字典,逐个传入测试用例来执行

import unittest
from ddt import ddt,file_data
from web_key_demo import KeyDemo

@ddt # 装饰ddt来管理测试类
class CaseDemo(unittest.TestCase):
    # 在测试用例执行前先执行
    def setUp(self) -> None:
        self.kd = KeyDemo('Chrome')
    # 测试用例执行后紧接着执行
    def tearDown(self) -> None:
        self.kd.quit()
    @file_data('./data/search.yaml') # ddt模块读取文件中数据转化为一组一组数据传入测试用例
    def test_01(self, url, _input, click): # 测试用例名字需要以test开头
        self.kd.open(url)
        self.kd.input(**_input)
        self.kd.click(**click)
        self.kd.sleep(3)

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

 yaml文件 数据层:

- # 一个符号表示一组数据,ddt读取后是一个字典
  url : http://www.baidu.com
  _input:
    name: id # _input下的一组数据,读取后字典键值对的value是一个字典
    value: kw
    txt: 美女
  click:
    name: id
    value: su
-
  url : http://www.baidu.com
  _input:
    name: id
    value: kw
    txt: 妹子
  click:
    name: id
    value: su
-
  url : http://www.jd.com
  _input:
    name: id
    value: key
    txt: 不可描述的好东西
  click:
    name: xpath
    value: //button[@aria-label='搜索']
search.yaml

五、测试套件

测试套件就是测试用例的集合,我们添加什么用例他就包含什么,执行顺序按照添加顺序

unittest.main()只能以默认顺序执行所有的测试用例,可以用到测试套件来执行一部分测试用例,并且顺序可以指定

 注意:测试套件的代码必须另外一个文件,不能直接写在测试用例文件的main内

import unittest # python自带,不需要额外安装
from ddt import ddt,data,file_data,unpack # 需要 pip install ddt


@ddt # 装饰ddt来声明我要用这个ddt来管理测试类
class CaseDemo(unittest.TestCase):

    # 如果希望从外边向测试用例类传参,就需要调用父类的init,从源码可以看到父类只有methodName='runTest'这么一个参数
    def __init__(self, methodName='runTest',wechat=None):
        super().__init__(methodName=methodName)
        self.wechat = wechat

    # 在测试用例执行前先执行
    def setUp(self) -> None:
        print('+++++++++++++++++++++++++++++++++++')

    # 测试用例执行后紧接着执行
    def tearDown(self) -> None:
        print('------------------------------------')

    def test_01(self):
        """test_01:注释可以显示在报告的描述信息中
        """
        print('print信息可以显示在成功的用例的测试报告中')

    @data('隔壁老王')  # 单个传参
    def test_03(self, name):
        print(name)

    @unittest.skip('我无条件跳过,就无条件跳过')
    def test_skip1(self):
        """test_skip1:注释可以显示在报告的描述信息中
        """
        print('test_skip1我还有机会BB么??')

    def test_assert(self):
        """'断言测试,msg是断言失败后提示信息,可以不要'
        """
        def _test():
            return '老王'

        res = _test()
        # 各种断言的方法都是内置属性,可以选择合适的来用

        self.assertEqual(res,'老赵', msg=f'不是老赵干的别冤枉他,是{res}干的') # 断言value1 == value2

        # # 失败后返回 AssertionError: '老王' not found in ['老张', '老李'] : 老王不在嫌疑人里面里面,不是他
        # self.assertIn(res,['老张','老李'],msg=f'{res}不在嫌疑人里面里面,不是他')

        # # 失败后返回: '老王' is not false : 返回不是False
        # self.assertFalse(res,msg='返回不是False')



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


    # # 在这个测试用例的main中这样添加套件,执行后你会发现还是执行了所有,并不是只执行了套件中添加的几个用例
    # # 在测试类的main函数中默认执行unittest.main()方法,套件不会生效,套件必须生成新的对象,即另外建立文件导入CaseDemo类来构建
    # import unittest
    # # 构造测试集
    # suite = unittest.TestSuite()
    # # 添加测试用例
    # suite.addTest(CaseDemo('test_skip1'))
    # suite.addTest(CaseDemo('test_assert'))
    # # suite.addTest(CaseDemo('test_03')) # 这个test_03用了ddt传参,不会运行。。。不知为何
    # # 执行测试
    # runner = unittest.TextTestRunner()
    # runner.run(suite)
case_demo03

添加测试套件有很多种方法,见下:(测试套件可以给测试用例传参,方法见test_skip1)

import unittest
from case_demo03 import CaseDemo

from ddt import ddt,data,file_data,unpack # 需要 pip install ddt

#构造测试集
suite = unittest.TestSuite()

# 套件添加测试用例 +++++++++++++++++++++++++++++++++++++++++++++++++++
# # 第一种方法:逐个添加
# # suite.addTest(CaseDemo('test_03')) # 这个test_03用了ddt传参,不会运行。。。不知为何
# suite.addTest(CaseDemo('test_skip1',wechat=666)) # 可以重写CaseDemo的init方法,就可以传参数过去
# suite.addTest(CaseDemo('test_assert'))

# 第二种方法:列表添加多个
cases = [CaseDemo('test_skip1',wechat='我是额外的参数'),
         CaseDemo('test_01'),
         CaseDemo('test_assert'),
         ]
suite.addTests(cases)
#
# # 第三种方法:批量添加测试用例类
# # 会执行路径下所有名字符合'case_demo*.py'的所有文件下测试用例类的所有测试用例
# execute_path = './'
# suite = unittest.defaultTestLoader.discover(start_dir=execute_path, pattern='case_demo*.py', top_level_dir=None)

# # 第四种方法:基于类对象添加测试用例类
# # 会执行类下面所有的测试用例
# suite.addTests(unittest.TestLoader().loadTestsFromTestCase(CaseDemo))

# # 第五种方法:基于类名称添加测试用例类
# # 会执行类下面所有的测试用例
# suite.addTests(unittest.TestLoader().loadTestsFromName('case_demo03.CaseDemo'))

# # 还有其他方法,用unittest.TestLoader()可以点出来
# unittest.TestLoader().loadTestsFromModule()
# unittest.TestLoader().loadTestsFromNames()

if __name__=='__main__':
    # 执行测试套件
    runner = unittest.TextTestRunner()
    runner.run(suite)

六、python使用BeautifulReport形成测试报告

对于测试用例的执行结果,我们可以使用beautifulreport来生成美观的html测试报告,经过使用,总结以下特点:

1.执行失败的错误信息可以直接显示在详情里面

2.执行成功的结果可以通过print来输出,就可以显示在测试报告详情里

3.测试用例的注释信息可以显示在报告中描述一栏

用法很简单,见下:

import unittest
from case_demo03 import CaseDemo

from ddt import ddt,data,file_data,unpack # 需要 pip install ddt

#构造测试集
suite = unittest.TestSuite()

# 套件添加测试用例 +++++++++++++++++++++++++++++++++++++++++++++++++++
# # 第一种方法:逐个添加
# # suite.addTest(CaseDemo('test_03')) # 这个test_03用了ddt传参,不会运行。。。不知为何
# suite.addTest(CaseDemo('test_skip1',wechat=666)) # 可以重写CaseDemo的init方法,就可以传参数过去
# suite.addTest(CaseDemo('test_assert'))

# 第二种方法:列表添加多个
cases = [CaseDemo('test_skip1',wechat='我是额外的参数'),
         CaseDemo('test_01'),
         CaseDemo('test_assert'),
         ]
suite.addTests(cases)
#
# # 第三种方法:批量添加测试用例类
# # 会执行路径下所有名字符合'case_demo*.py'的所有文件下测试用例类的所有测试用例
# execute_path = './'
# suite = unittest.defaultTestLoader.discover(start_dir=execute_path, pattern='case_demo*.py', top_level_dir=None)

# # 第四种方法:基于类对象添加测试用例类
# # 会执行类下面所有的测试用例
# suite.addTests(unittest.TestLoader().loadTestsFromTestCase(CaseDemo))

# # 第五种方法:基于类名称添加测试用例类
# # 会执行类下面所有的测试用例
# suite.addTests(unittest.TestLoader().loadTestsFromName('case_demo03.CaseDemo'))

# # 还有其他方法,用unittest.TestLoader()可以点出来
# unittest.TestLoader().loadTestsFromModule()
# unittest.TestLoader().loadTestsFromNames()

if __name__=='__main__':
    # # 执行测试套件
    # runner = unittest.TextTestRunner()
    # runner.run(suite)

    # 使用BeautifulReport执行套件并生成测试报告
    from BeautifulReport import BeautifulReport

    result = BeautifulReport(suite)
    # filename:测试报告报告名字
    # description:测试用例名字,会显示在图文进面
    # report_dir:报告存储路径,没有会自动创建
    # theme:默认即可,可不写
    result.report(filename='wxbot原子操作测试报告', description='测试deafult报告', report_dir='report', theme='theme_default')

生成的报告如下图:

 

 

 

posted @ 2021-12-12 22:22  www.pu  Views(191)  Comments(0Edit  收藏  举报