unittest框架

x33g5p2x  于2022-06-13 转载在 其他  
字(13.8k)|赞(0)|评价(0)|浏览(263)

一、unittest框架各组件作用及关系

unittest 是web界面的单元测试框架。而Junit是白盒测试的单元测试框架。
unittest 单元测试提供了创建测试用例,测试套件以及批量执行的方案, unittest 在安装pyhton 以后就直接自带了,直接import unittest 就可以使用。

作为单元测试的框架, unittest 也是可以对程序最小模块的一种敏捷化的测试。在自动化测试中,我们虽然不需要做白盒测试,但是必须需要知道所使用语言的单元测试框架。利用单元测试框架,创建一个类,该类继承unittest的TestCase,这样可以把每个case看成是一个最小的单元, 由测试容器组织起来,到时候直接执行,同时引入测试报告。

unittest各组件的关系:

test fixture:初始化和清理测试环境,比如创建临时的数据库,文件和目录等,其中 setUp() 和 tearDown()是最常用的方法
test case:单元测试用例,TestCase 是编写单元测试用例最常用的类
test suite:单元测试用例的集合,TestSuite 是最常用的类
test runner:执行单元测试
test report:生成测试报告

二、unittest中TestCase的使用

import time
from selenium import webdriver
import unittest
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoAlertPresentException

#继承
class testCase1(unittest.TestCase):
    def setUp(self):
        self.driver = webdriver.Chrome()
        self.url = "https://www.baidu.com/"
        self.driver.get(self.url)
        self.driver.maximize_window()
        time.sleep(3)

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

    def test_baidu1(self):
        driver = self.driver
        driver.find_element(By.ID, "kw").send_keys("孔刘")
        driver.find_element(By.ID, "su").click()
        time.sleep(3)

    def test_baidu2(self):
        driver = self.driver
        driver.find_element(By.LINK_TEXT, "新闻").click()
        time.sleep(3)

    def is_alert_exist(self):
        try:
            self.driver.switch_to.alert
        except NoAlertPresentException as e:
            return False
        return True

    if __name__ == "__main__":
        unittest.main(verbosity=0)

关注的点:
1.创建一个类,要它继承于unittest中的TestCase类,才能完成下面的步骤。
2.setUp方法,在执行每一个以test_开头的方法前都会进行;而tearDown方法在执行每一个以test_开头的方法后都会进行。setUp方法是进行初始化工作,tearDown是进行清理工作
3.需要测试的流程关键部分放到以test_开头的方法中,那么编译器就会自动去执行。
4.不是以test_开头的方法,是需要调用才能够执行,否则不执行。
5.对于测试流程中有alert、prompt等出现提示框的,要捕捉异常。
6.写出main方法,给程序一个执行的入口,verbosity关系到打印的结果详细程度。
7.如果在不同的TestCase下右击,即是选择执行的是哪个TestCase,同样地,可以在下图的地方选择执行的代码。

三、unittest中TestSuite的使用

1.批量执行脚本

完整的单元测试很少只执行一个测试用例,开发人员通常都需要编写多个测试用例才能对某一软件功能进行比较完整的测试,这些相关的测试用例称为一个测试用例集,在unittest中是用TestSuite 类来表示的。

1.1 addTest()方法的使用

test001的Python文件:
test002的Python跟下面的相同,只不过将类名改为了testCase2.

import time
from selenium import webdriver
import unittest
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoAlertPresentException

#继承
class testCase1(unittest.TestCase):
    def setUp(self):
        self.driver = webdriver.Chrome()
        self.url = "https://www.baidu.com/"
        self.driver.get(self.url)
        self.driver.maximize_window()
        time.sleep(3)

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

    def test_baidu1(self):
        driver = self.driver
        driver.find_element(By.ID, "kw").send_keys("孔刘")
        driver.find_element(By.ID, "su").click()
        time.sleep(3)

    def test_baidu2(self):
        driver = self.driver
        driver.find_element(By.LINK_TEXT, "新闻").click()
        time.sleep(3)

    def is_alert_exist(self):
        try:
            self.driver.switch_to.alert
        except NoAlertPresentException as e:
            return False
        return True

    if __name__ == "__main__":
        unittest.main(verbosity=0)

createSuite的Python文件:

import unittest
from src202205 import test001
from src202205 import test002

def createSuite():
    suite = unittest.TestSuite()
    suite.addTest(test001.testCase1("test_baidu1"))
    suite.addTest(test001.testCase1("test_baidu2"))
    suite.addTest(test002.testCase2("test_baidu1"))
    suite.addTest(test002.testCase2("test_baidu2"))
    return suite

if __name__ == "__main__":
    suite = createSuite()
    runner = unittest.TextTestRunner(verbosity=2) # 打印的很详细
    runner.run(suite)

执行顺序下面会讲到。

1.2 makeSuite方法

这个方法是将一个类中的所有以test_开头的方法全部放入到suite套件中,能够解决将类中如果有很多的以test_开头的方法不用一一添加到套件的问题。注意makeSuite方法的使用。

import unittest
from src202205 import test001
from src202205 import test002

def createSuite():
    suite = unittest.TestSuite()
    suite.addTest(unittest.makeSuite(test001.testCase1))
    suite.addTest(unittest.makeSuite(test002.testCase2))
    return suite

if __name__ == "__main__":
    suite = createSuite()
    runner = unittest.TextTestRunner(verbosity=2) # 打印的很详细
    runner.run(suite)

1.3 TestLoader()方法

对于TestLoader方法来说,是先将类装入小的测试套件中,再将全部小的测试套件装入最终的一个最大的测试套件中,在将小的测试套件装入大的测试套件中,可以采用列表的形式装入。

import unittest
from src202205 import test001
from src202205 import test002

def createSuite():
    suite = unittest.TestSuite()
    suite1 = unittest.TestLoader().loadTestsFromTestCase(test001.testCase1)
    suite2 = unittest.TestLoader().loadTestsFromTestCase(test002.testCase2)
    suite = unittest.TestSuite([suite1, suite2])
    return suite

if __name__ == "__main__":
    suite = createSuite()
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

1.4 discover()方法

discover方法可以在指定文件下,所有特定格式命名的测试脚本中类的所有以test_开头的方法存放起来。
下面的代码中,test00开头的文件都是放在src202205的文件夹中。

import unittest

def createSuite():
    discover = unittest.defaultTestLoader.discover('../src202205', pattern='test00*.py', top_level_dir=None)
    #要先返回上一级,然后找到以test00开头,.py结尾的文件
    return discover

if __name__ == "__main__":
    suite = createSuite()
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

四、用例的执行顺序

unittest 框架默认加载测试用例的顺序是根据ASCII 码的顺序,数字与字母的顺序为: 0-9,A-Z,a-z。越小的先运行。并且如果有baidu和baidU,则baidU的会先运行。
所以, TestAdd 类会优先于TestBdd 类被发现, test_aaa() 方法会优先于test_ccc() 被执行。对于测试目录与测试文件来说, unittest 框架同样是按照这个规则来加载测试用例。

五、忽略用例执行

只需要在不想执行的用例前加上@unittest.skip(内容)即可。写入的内容中,在测试完其它用例的时候会打印出来。

六、unittest断言

自动化的测试中, 对于每个单独的case来说,一个case的执行结果中, 必然会有期望结果与实际结果, 来判断该case是通过还是失败, 在unittest 的库中提供了大量的实用方法来检查预期值与实际值, 来验证case的结果, 一般来说, 检查条件大体分为等价性, 逻辑比较以及其他, 如果给定的断言通过, 测试会继续执行到下一行的代码, 如果断言失败, 对应的case测试会立即停止或者生成错误信息( 一般打印错误信息即可) ,但是不要影响其他的case执行。

断言方法断言描述
assertEqual(arg1, arg2, msg=None)验证arg1=arg2,不等则fail
assertNotEqual(arg1, arg2, msg=None)验证arg1 != arg2, 相等则fail
assertTrue(expr, msg=None)验证expr是true,如果为false,则fail
assertFalse(expr,msg=None)验证expr是false,如果为true,则fail
assertIs(arg1, arg2, msg=None)验证arg1、arg2是同一个对象,不是则fail
assertIsNot(arg1, arg2, msg=None)验证arg1、arg2不是同一个对象,是则fail
assertIsNone(expr, msg=None)验证expr是None,不是则fail
assertIsNotNone(expr, msg=None)验证expr不是None,是则fail
assertIn(arg1, arg2, msg=None)验证arg1是arg2的子串,不是则fail
assertNotIn(arg1, arg2, msg=None)验证arg1不是arg2的子串,是则fail
assertIsInstance(obj, cls, msg=None)验证obj是cls的实例,不是则fail
assertNotIsInstance(obj, cls, msg=None)验证obj不是cls的实例,是则fail

我们还可以通过IDE来增加断言:当结束录制后,保存的脚本就有了断言的代码。

七、HTML报告生成

脚本执行完毕之后,还需要看到HTML报告,下面我们就通过HTMLTestRunner.py 来生成测试报告。这个HTMLTestRunner.py 需要我们手动去添加到C:\Program Files\Python\Lib这个目录中(Python的安装目录),才能看到测试报告。

在src202205文件夹中,创建一个HTMLReport文件(名字不作要求):

import unittest
import sys, os
import HTMLTestRunner
import time

def createSuite():
    discover = unittest.defaultTestLoader.discover('../src202205', pattern='test00*.py', top_level_dir=None)
    print(discover)
    return discover

if __name__ == "__main__":
    # 获取当前文件的路径
    curpath = sys.path[0]
    print(curpath)
    if not os.path.exists(curpath+"/resultReport"):
        os.mkdir(curpath+"/resultReport")
    now = time.strftime("%Y-%m-%d-%H %M %S", time.localtime(time.time()))
    filename = curpath+"/resultReport/"+now+"-"+"resultReport.html"
    with open(filename, 'wb') as fp:
        runner = HTMLTestRunner.HTMLTestRunner(stream=fp, title=u"测试报告",
                                               description=u"测试用例执行的结果", verbosity=2)
        suite = createSuite()
        runner.run(suite)

细节:
1.curpath = sys.path[0]打印出来的路径是D:\pythonProject\0530\src202205,即这个HTMLReport文件的路径。目的是在该路径底下先创建一个resultReport的文件夹,来保存测试报告。
2.now = time.strftime("%Y-%m-%d-%H %M %S", time.localtime(time.time()))是格式化时间,time.time()获取的是时间戳,time.localtime(time.time())将时间戳转化成现在的时间,time.strftime("%Y-%m-%d-%H %M %S", time.localtime(time.time()))是将现在的时间格式化为年-月-日- 时 分 秒,能够使得测试报告的名字更加清晰。
3.with open(filename, 'wb') as fp:打开一个文件,以wb的形式去写入,fp为输入流。
4.runner = HTMLTestRunner.HTMLTestRunner(stream=fp, title=u"测试报告",description=u"测试用例执行的结果", verbosity=2),stram是写入文件流,title是给该html文件起标题,description对测试的结果的描述,verbosity=2是对测试报告结果信息复杂度。

如上面代码打印的测试报告如下:可以看到对测试结果的总结是非常有帮助的。

八、对于测试遇到错误后的自动截图

用例不可能每一次运行都成功,肯定运行时候有不成功的时候。如果可以捕捉到错误,并且把错误截图保存,这将是一个非常棒的功能,也会给我们错误定位带来方便。

下面的代码中,利用断言利用driver.title来判断打开的页面是否为百度的搜索页面,如果不是,就截图当前测试打开的页面是一个什么页面,将截图到的图片放到一个指定的文件夹中保存,并给该图片命名。
注:一定要注意路径的问题。

import os.path
import time
from selenium import webdriver
import unittest
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoAlertPresentException

#继承
class testCase1(unittest.TestCase):
    def setUp(self):
        self.driver = webdriver.Chrome()
        self.url = "https://www.baidu.com/"
        self.driver.get(self.url)
        self.driver.maximize_window()
        time.sleep(3)

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

    def test_baidu2(self):
        driver = self.driver
        try:
            self.assertEqual(driver.title,"百度,你就知道",msg="判断失败,没有打开百度页面")
        except:
            self.save_error_image(driver, "baidu.png")
        time.sleep(3)

    def save_error_image(self, driver, name):
        if not os.path.exists("./errorImage"):
            os.makedirs("./errorImage")
        now = time.strftime("%Y%m%d-%H-%M-%S", time.localtime(time.time()))
        self.driver.get_screenshot_as_file('./errorImage/'+now+"-"+name)

    def is_alert_exist(self):
        try:
            self.driver.switch_to.alert
        except NoAlertPresentException as e:
            return False
        return True

    if __name__ == "__main__":
        unittest.main(verbosity=0)

如果断言中判断失败,就在目录中产生了一个errorImage的目录,目录中就放入了在断言中失败时,测试打开的页面。
目录关系:

九、数据驱动

之前的case都是数据和代码在一起编写。考虑如下场景:
需要多次执行一个案例,比如baidu搜索,分别输入中文、英文、数字等进行搜索这时候需要编写3个案例吗?有没有版本一次运行?
python 的unittest 没有自带数据驱动功能。所以如果使用unittest,同时又想使用数据驱动,那么就可以使用DDT来完成。

可以在cmd窗口中输入pip show ddt 中查看是否安装了ddt,如果没有安装会出现一个warning,安装ddt:pip install ddt ,就进行安装。

安装好ddt后,在PyCharm中就可以直接import ddt 了,它能够实现类似于代码的复用,耦合性更强。

使用ddt前,导入ddt中的ddt、data、file_name、unpack
在使用ddt的时候,必须要在类的前面加上@ddt,才能说明这个类是使用ddt了的。

1.使用ddt.data

如:可以看到,test_baidu1的方法中增加了一个value参数,即该方法上面的@data() 中的数据,一个一个地将data中的数据传入给参数value,就能够避免大量重复的代码了。

import time
from selenium import webdriver
import unittest
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoAlertPresentException
from ddt import ddt, file_data, unpack, data
#继承
@ddt
class testCase1(unittest.TestCase):
    def setUp(self):
        self.driver = webdriver.Chrome()
        self.url = "https://www.baidu.com/"
        self.driver.get(self.url)
        self.driver.maximize_window()
        time.sleep(3)

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

    @data("Lisa", "马思维")
    def test_baidu1(self, value):
        driver = self.driver
        driver.find_element(By.ID, "kw").send_keys(value)
        driver.find_element(By.ID, "su").click()
        time.sleep(3)

    def is_alert_exist(self):
        try:
            self.driver.switch_to.alert
        except NoAlertPresentException as e:
            return False
        return True

    if __name__ == "__main__":
        unittest.main(verbosity=0)

2.使用ddt.file_data

有时候,我们可能会测试大量的数据,因此就不可能直接在@data()中输入大量的内容,会显得代码很乱,因此可以将上面的内容写入到一个json文件里面,以json的形式来保存数据。
(此时json文件和此py文件是同一级文件夹下的),因此可以直接写@file_data("test_baidu.json")

import time
from selenium import webdriver
import unittest
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoAlertPresentException
from ddt import ddt, file_data, unpack, data
#继承
@ddt
class testCase1(unittest.TestCase):
    def setUp(self):
        self.driver = webdriver.Chrome()
        self.url = "https://www.baidu.com/"
        self.driver.get(self.url)
        self.driver.maximize_window()
        time.sleep(3)

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

    # @data("Lisa", "马思维")
    @file_data("test_baidu.json")
    def test_baidu1(self, value):
        driver = self.driver
        driver.find_element(By.ID, "kw").send_keys(value)
        driver.find_element(By.ID, "su").click()
        time.sleep(3)

    @unittest.skip("skipping")
    def test_baidu2(self):
        driver = self.driver
        driver.find_element(By.LINK_TEXT, "新闻").click()
        time.sleep(3)

    def is_alert_exist(self):
        try:
            self.driver.switch_to.alert
        except NoAlertPresentException as e:
            return False
        return True

    if __name__ == "__main__":
        unittest.main(verbosity=0)

test_baidu.json中的内容:

3.使用ddt.unpack

有时候,我们要使用的数据是对应的,如:此时要断言网页的title和我们预计的一不一样,不一样就报错。
利用unpack也可以使用解耦合。当一个列表中有多个数据时,就使用unpack来处理了。

import time
from selenium import webdriver
import unittest
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoAlertPresentException
from ddt import ddt, file_data, unpack, data
#继承
@ddt
class testCase1(unittest.TestCase):
    def setUp(self):
        self.driver = webdriver.Chrome()
        self.url = "https://www.baidu.com/"
        self.driver.get(self.url)
        self.driver.maximize_window()
        time.sleep(3)

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

    # @data("Lisa", "马思维")
    @data(["Jolin","Jolin_百度搜索"],["马思维","马思维_百度搜索"])
    @unpack
    def test_baidu1(self, value1, value2):
        driver = self.driver
        driver.find_element(By.ID, "kw").send_keys(value1)
        driver.find_element(By.ID, "su").click()
        time.sleep(3)
        self.assertEqual(driver.title, value2, msg="fail!")
        time.sleep(3)

    def is_alert_exist(self):
        try:
            self.driver.switch_to.alert
        except NoAlertPresentException as e:
            return False
        return True

    if __name__ == "__main__":
        unittest.main(verbosity=0)

同样地,测试的数据不可能是一两条,因此就可以把@data(["Jolin","Jolin_百度搜索"],["马思维","马思维_百度搜索"])放入到一个.txt文件中保存,再在需要这个数据时就读取该文件。注:在.txt文件中,在首行要加上Data,第二行才开始写数据。

文件目录关系:

import csv
import sys
import time
from selenium import webdriver
import unittest
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoAlertPresentException
from ddt import ddt, file_data, unpack, data

def getCsv(file_name):
    rows=[]
    path=sys.path[0]
    print(path)
    with open(path + '/src202205/' + '/data/'+file_name, 'rt',encoding='utf-8') as f:
        readers = csv.reader(f, delimiter=',', quotechar='|')
        next(readers, None)
        for row in readers:
            temprows=[]
            for i in row:
                temprows.append(i)
            rows.append(temprows)
        return rows

@ddt
class testCase(unittest.TestCase):
    def setUp(self):
        self.driver = webdriver.Chrome()
        self.url = "https://www.baidu.com/"
        self.driver.get(self.url)
        self.driver.maximize_window()
        time.sleep(3)

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

    # @data(["Jolin","Jolin_百度搜索"],["马思维","马思维_百度搜索"])
    @data(*getCsv('test_baidu_data.txt'))
    @unpack
    def test_baidu1(self, value, excepted_value):
        driver = self.driver
        driver.find_element(By.ID, "kw").send_keys(value)
        driver.find_element(By.ID, "su").click()
        driver.maximize_window()
        time.sleep(3)
        self.assertEqual(driver.title, excepted_value, msg='fail!!')
        time.sleep(3)

    def is_alert_exist(self):
        try:
            self.driver.switch_to.alert
        except NoAlertPresentException as e:
            return False
        return True

    if __name__ == "__main__":
        unittest.main(verbosity=0)

对于readers = csv.reader(f, delimiter=',', quotechar='|')中的意思是:f为读取的流,delimiter是以什么为分隔符,quotechar中的| 为换行符。
next(readers, None)就是开始读取该.txt文件的意思。

此处需要注意.txt的路径问题,读取的该文件时要确定好对应的路径。如果报错的时候就看控制台中提供的信息,去更准确地定位问题。

相关文章