单元测试框架


一.使用的库

在Python语?中应?最?泛的单元测试框架是unittest和pytest,unittest属于标准库,只要安装了Python解释器后就可以直接导?使?了,pytest是第三?的库,需要单独的安装。

二.unittest标准库的使用

  • 组件的介绍

    • 测试用例(testCase):编写的每个具体的测试场景

      • 每个测试用例必须是test开头,建议为test_

      • 每个测试用例都应该有标题

      • 每个测试用例建议只写一个测试场景

      • 每个测试用例必须要有期望结果,也就是断言,否则就没有意义

      • 每个测试用例都是独立的,不会因为业务的依赖关系而关系

    • 测试固件(勾子方法):初始化和清理

    • 测试套件(testSuite):测试套件是testCase的集合

    • 测试执行(testRunner):具体执行被编写的测试套件或者是测试用例

    • 测试报告(testReport):测试报告反馈实际的测试结果

  • 自动化测试用例要素跟顺序:

    1. 前提条件/初始化:setUp

    2. 测试步骤

    3. 验证结果(断言)

    4. 清理:tearDown()

  • unittest属性

    • 属性的查看

      import unittest
      
      print(dir(unittest))

       输出结果:

      ['BaseTestSuite', 'FunctionTestCase', 'IsolatedAsyncioTestCase', 'SkipTest', 'TestCase', 'TestLoader', 'TestProgram', 'TestResult', 'TestSuite', 'TextTestResult', 'TextTestRunner', '_TextTestResult', '__all__', '__builtins__', '__cached__', '__dir__', '__doc__', '__file__', '__getattr__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__unittest', 'addModuleCleanup', 'case', 'defaultTestLoader', 'expectedFailure', 'findTestCases', 'getTestCaseNames', 'installHandler', 'load_tests', 'loader', 'main', 'makeSuite', 'registerResult', 'removeHandler', 'removeResult', 'result', 'runner', 'signals', 'skip', 'skipIf', 'skipUnless', 'suite', 'util']
    • unittest.TestCase

      import unittest
      class ApiTest(unittest.TestCase): 
          def test_001(self): 
              pass

       TestCase类,所有测试用例类继承的基本类。

    • unittest.main()

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

      可以方便的将一个单元测试模块变为可直接运行的测试脚本,main()方法使用TestLoader类来搜索所有包含在该模块中以“test”命名开头的测试方法,并自动执行他们。

      在unittest的main中verbosity有三个参数,分别是0,1,2,代表的意思具体如下:
          0(静默模式):仅仅获取总的测试?例数以及总的结果
          1(默认模式)
          2(详细模式):测试结果会显示每个测试?例的所有相关信息    
    • setUp()

      def setUp(self) -> None:
              self.driver=webdriver.Chrome()
              self.driver.maximize_window()
              self.driver.get('http://www.baidu.com')
              self.driver.implicitly_wait(10)

       用于测试用例执行前的初始化工作,如测试用例需要登录web,可以先初始化浏览器。

    • tearDown()

      def tearDown(self) -> None:
              self.driver.quit()

      用于测试用例执行之后的善后工作,如关闭浏览器等

    • testLoder()

      if __name__ == '__main__':
          suite = unittest.TestLoader().loadTestsFromModule('test_baidu2.py')
          unittest.TextTestRunner().run(suite)

      用来加载 TestCase到TestSuite中,方法为:

      def loadTestsFromTestCase(self, testCaseClass)
      
      def loadTestsFromModule(self, module, *args, pattern=None, **kws)
      
      def loadTestsFromName(self, name, module=None)
      
      def loadTestsFromNames(self, names, module=None)
      
      def getTestCaseNames(self, testCaseClass)
      
      def discover(self, start_dir, pattern='test*.py', top_level_dir=None)
      
      def _get_directory_containing_module(self, module_name)
      
      def _get_name_from_path(self, path)
      
      def _get_module_from_name(self, name)
      
      def _match_path(self, path, full_path, pattern)
      
      def _find_tests(self, start_dir, pattern, namespace=False)
      
      def _find_test_path(self, full_path, pattern, namespace=False)
    • TextTestRunner

          unittest.TextTestRunner().run(suite)

       按模块/类执行测试代码时需要使用到TextTestRunner 中的run 的方法去执行。

    • assert *():断言

      assert:python解释器自带的断言,unittest中使用断言需要封装
      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的一个实例,不是则测试用例通过。
  • 测试固件

    • 只执行一次(类方法)

      #! /usr/bin/env python
      # -*- coding:utf-8 -*-
      # author:特昂糖
      from selenium import webdriver
      import unittest
      
      class BaiduTest(unittest.TestCase):
          #测试固件
          @classmethod
          def setUpClass(cls) -> None:
              cls.driver=webdriver.Chrome()
              cls.driver.maximize_window()
              cls.driver.get('http://www.baidu.com')
              cls.driver.implicitly_wait(10)
      
          @classmethod
          def tearDownClass(cls) -> None:
              cls.driver.quit()
    • 每次都执行

      #! /usr/bin/env python
      # -*- coding:utf-8 -*-
      # author:特昂糖
      
      from selenium import webdriver
      import unittest
      
      class BaiduTest(unittest.TestCase):
          def setUp(self) -> None:
              self.driver=webdriver.Chrome()
              self.driver.maximize_window()
              self.driver.get('http://www.baidu.com')
              self.driver.implicitly_wait(20)
      
          def tearDown(self) -> None:
              self.driver.quit()

      代码执行顺序为:测试固件--->测试套件,测试固件--->测试套件

  • 测试套件

    • 测试套件的执行

      • 按测试用例执行

      • 按测试类执行

        #! /usr/bin/env python
        # -*- coding:utf-8 -*-
        # author:特昂糖
        
        from selenium import webdriver
        import unittest
        
        class BaiduTest(unittest.TestCase):
            def setUp(self) -> None:
                self.driver=webdriver.Chrome()
                self.driver.maximize_window()
                self.driver.get('http://www.baidu.com')
                self.driver.implicitly_wait(20)
        
            def tearDown(self) -> None:
                self.driver.quit()
        
            def test_baidu_title(self):
                '''验证百度首页的title'''
                self.assertEqual(self.driver.title,'百度一下,你就知道')
        
            def test_baidu_url(self):
                '''验证百度首页的URL'''
                self.driver.current_url=='https://www.baidu.com/'
        
            def test_baidu_video(self):
                '''验证点击视频之后跳转的页面'''
                nowhandler = self.driver.current_window_handle
                self.driver.find_element_by_link_text('视频').click()
                allhandlers = self.driver.window_handles
                for handler in allhandlers:
                    if handler != nowhandler:
                        self.driver.switch_to.window(handler)
                        self.assertEqual(self.driver.current_url, 'https://haokan.baidu.com/?sfrom=baidu-top')
        
            def test_baidu_map(self):
                '''验证点击地图后跳转的页面'''
                nowhandler=self.driver.current_window_handle
                self.driver.find_element_by_link_text('地图').click()
                allhandlers=self.driver.window_handles
                for handler in allhandlers:
                    if handler!=nowhandler:
                        self.driver.switch_to.window(handler)
                        self.assertTrue(self.driver.current_url.startswith('https://map.baidu'))
        
            def test_baidu_hao123(self):
                '''验证点击hao123后跳转的页面'''
                nowhandler=self.driver.current_window_handle
                self.driver.find_element_by_link_text('hao123').click()
                allhandlers=self.driver.window_handles
                for handler in allhandlers:
                    if handler!=nowhandler:
                        self.driver.switch_to.window(handler)
                        self.assertEqual(self.driver.current_url,'https://www.hao123.com/')
        
        if __name__ == '__main__':
            suite=unittest.TestLoader().loadTestsFromTestCase(BaiduTest)
            unittest.TextTestRunner().run(suite)
      • 按测试模块执行

        import unittest
        import  os
        
        def getTests():
            '''加载路径下所有的测试模块'''
            suite=unittest.TestLoader().discover(start_dir=os.path.dirname(__file__),pattern='test_*.py')
            return suite
        
        def runSuite():
            unittest.TextTestRunner().run(getTests())
        
        if __name__ == '__main__':
            runSuite()
  • 测试套件的优化

    • 测试套件的分离

      from selenium import webdriver
      import unittest
      class Init(unittest.TestCase):
          @classmethod
          def setUpClass(cls) -> None:
              cls.driver = webdriver.Chrome()
              cls.driver.maximize_window()
              cls.driver.get('http://www.baidu.com')
              cls.driver.implicitly_wait(10)
      
          @classmethod
          def tearDownClass(cls) -> None:
              cls.driver.quit()

      把测试套件分离为一个方法,用的时候直接调用继承就可以了

    • 源代码的继承

      #! /usr/bin/env python
      # -*- coding:utf-8 -*-
      # author:特昂糖
      
      from test.init import Init
      
      class BaiduTest(Init):
      
          def test_baidu_title(self):
              '''验证百度首页的title'''
              self.assertEqual(self.driver.title,'百度一下,你就知道')
      
          def test_baidu_url(self):
              '''验证百度首页的URL'''
              self.driver.current_url=='https://www.baidu.com/'

      使用的方法是类继承,先导入方法,再继承

  • unittest的参数化

    • 使用的库

      在unittest测试框架中,参数化使用的库为:parameterized 
      安装方式为:pip3 install parameterized
    • 解决的问题

      参数化:
          我们把相同的测试步骤,不同的测试场景,可以使用参数化解决的问题是可以使用少量的测试代码,来覆盖更多的测试场景
    • 实战案例

       以新浪邮箱的登录页面为例,我们需要测试不同的测试场景,如账号密码为空提示、邮箱号格式错误的提示、账号密码不符的错误提示,使用之前的方法,我们需要写三个测试用例,代码如下:

      
      
      #! /usr/bin/env python
      # -*- coding:utf-8 -*-
      # author:特昂糖
      import time
      from selenium import webdriver
      import unittest
      
      class SinaLogin(unittest.TestCase):
          def setUp(self) -> None:
              self.driver = webdriver.Chrome()
              self.driver.maximize_window()
              self.driver.get("https://mail.sina.com.cn/")
              self.driver.implicitly_wait(20)
      
          def tearDown(self) -> None:
              self.driver.quit()
      
          def test_sina_null(self):
              '''sina邮箱验证,登录账号秘密为空'''
              self.driver.find_element_by_class_name('loginBtn').click()
              divText=self.driver.find_element_by_xpath('/html/body/div[3]/div/div[2]/div/div/div[4]/div[1]/div[1]/div[1]/span[1]')
              self.assertEqual(divText.text,'请输入邮箱名')
      
          def test_sina_email_format(self):
              '''sina邮箱验证,登录邮箱格式不正确'''
              self.driver.find_element_by_id('freename').send_keys('jasbahc12')
              self.driver.find_element_by_class_name('loginBtn').click()
              divText=self.driver.find_element_by_xpath('/html/body/div[3]/div/div[2]/div/div/div[4]/div[1]/div[1]/div[1]/span[1]')
              self.assertEqual(divText.text,'您输入的邮箱名格式不正确')
      
          def test_sina_username_error(self):
              '''sina邮箱验证,账号密码不匹配'''
              self.driver.find_element_by_id('freename').send_keys('hc12@sina.cn')
              self.driver.find_element_by_id('freepassword').send_keys('jasbahc12')
              self.driver.find_element_by_class_name('loginBtn').click()
              time.sleep(1)
              divText=self.driver.find_element_by_xpath('/html/body/div[3]/div/div[2]/div/div/div[4]/div[1]/div[1]/div[1]/span[1]')
              self.assertEqual(divText.text,'登录名或密码错误')
      
      if __name__ == '__main__':
          unittest.main(verbosity=2)
      针对以上的代码,我们可以使用参数化的方式把代码改良,只需要写一个测试用例就可以
      import time
      from selenium import webdriver
      import unittest
      from parameterized import parameterized,param
      
      class SinaLogin(unittest.TestCase):
          def setUp(self) -> None:
              self.driver = webdriver.Chrome()
              self.driver.maximize_window()
              self.driver.get("https://mail.sina.com.cn/")
              self.driver.implicitly_wait(20)
      
          def tearDown(self) -> None:
              self.driver.quit()
      
          @parameterized.expand([
              param('','','请输入邮箱名'),
              param('asqs121','','您输入的邮箱名格式不正确'),
              param('wxni121x@sina.cn','as12','登录名或密码错误')])  #参数化
      
          def test_sina_Login(self,username,password,result):
              '''sina邮箱登录验证'''
              self.driver.find_element_by_id('freename').send_keys(username)
              self.driver.find_element_by_id('freepassword').send_keys(password)
              self.driver.find_element_by_class_name('loginBtn').click()
              time.sleep(1)
              divText=self.driver.find_element_by_xpath('/html/body/div[3]/div/div[2]/div/div/div[4]/div[1]/div[1]/div[1]/span[1]')
              self.assertEqual(divText.text,result)
      
      
      if __name__ == '__main__':
          unittest.main(verbosity=2) 
  • unittest测试报告

  • 运行test.py之后得到的测试结果无法直接提交,我们需要借助第三方的库生成HTML格式的测试报告,这里我们用到的是HTMLTestRunner

 

  • 下载地址为:https://github.com/tungwaiyip/HTMLTestRunner

 

  • 下载之后我们需要把这个文件放到Python安装路径的Lib文件夹下

 

  • 测试报告运行代码如下:
    #! /usr/bin/env python
    # -*- coding:utf-8 -*-
    # author:特昂糖
    import  os
    import  time
    import unittest
    import  HTMLTestRunner
    
    def allTests():             #查找路径下所有的测试文件
        suite=unittest.TestLoader().discover(
            start_dir=os.path.dirname(__file__),
            pattern='test_*.py',
        )
        return suite
    
    def base_dir():             #查看文件路径的上一级
        return os.path.dirname(os.path.dirname(__file__))
    
    def run():
        fp=open(os.path.join(base_dir(),'report','report.html'),'wb')   #路径拼接和文件二级制的写入
        runner=HTMLTestRunner.HTMLTestRunner(
            stream=fp,
            title='UI自动化测试报告',
            description='UI自动化测试报告详细信息'
        )
        runner.run(allTests())
    
    if __name__ == '__main__':
        getNowTime()

    测试报告生成后会写入report.html的文件中,我们使用浏览器打开就可以查看:

    我们发现上面的代码生成的测试报告每次都会覆盖,这样我们想查看之前的测试报告就没了,我们可以在生成的测试报告名称中加入时间,代码如下
    import  os
    import  time
    import unittest
    import  HTMLTestRunner
    
    def allTests():             #查找路径下所有的测试文件
        suite=unittest.TestLoader().discover(
            start_dir=os.path.dirname(__file__),
            pattern='test_*.py',
        )
        return suite
    
    def getNowTime():               #生成当前时间
        return time.strftime('%y-%m-%d %H_%M_%S',time.localtime(time.time()))
    
    def base_dir():             #查看文件路径的上一级
        return os.path.dirname(os.path.dirname(__file__))
    
    def run():
        fp=open(os.path.join(base_dir(),'report',getNowTime()+'report.html'),'wb')   #路径拼接和文件的
        runner=HTMLTestRunner.HTMLTestRunner(
            stream=fp,
            title='UI自动化测试报告',
            description='UI自动化测试报告详细信息'
        )
        runner.run(allTests())
    
    if __name__ == '__main__':
        run()