unittest单元测试框架

章节概述:
单元测试是一项对技术要求非常高的工作,只有白盒测试人员和软件开发人员能够胜任,但使用单元测试框架进行单元测试并不简单,单元测试框架不仅是单元测试,也是不同类型的让我们看看提供了什么功能。
1、提供用例组织和执行:
在Python中,我们写的代码可以定义类、方法和函数,所以如何定义测试用例? 如何灵活地=控制这些测试用例的执行? 单元测试的框架会告诉我们。
2、提供丰富的断言方法:
进行功能测试时,测试用例需要预期的结果。如果测试用例的执行结果与预期的结果不一致,则判断为测试用例失败。在自动化测试中,用“断言”来判断测试用例的执行是否成功。常用的单元测试框架提供了丰富的断言方法。例如,判断不等于、包含/不包含、真/假等。
3、提供丰富的日志:
运行结果非常重要,因为自动化测试在运行过程中不需要手动干预。我们有必要从不同的结果中清楚地看到失败的原因。另外,还需要统计总运行时间、失败测试用例数、成功测试用例数等测试用例的运行结果。这些功能也由单元测试框架提供。
从以上几点来看,单元测试框架不仅可以用来编写测试用例,还可以用来完成所有涉及自动化测试的任务,如Web自动化测试、App自动化测试、界面自动化测试以及接口自动化测试等。
认识unittest:
Python中有很多unittest框架,比如doctest、unittest、pytest、nose等。Python2.1和更高版本已经将unittest作为Python开发包中的标准模块。
1、认识单元测试:
没有单元测试框架可以写单元测试吗?答案是肯定的。单元测试本质上是通过一段代码来验证另一段代码,所以你可以不用单元测试框架来编写单元测试。以下是一个例子:

class Calculator:
    """ 用于完成两个数的加、减、乘、除"""
    
    def __init__(self, a, b):
        
        self.a = int(a)
        self.b = int(b)
        
    # 加法
    def add(self):
        return self.a + self.b
    
    # 减法
    def sub(self):
        return self.a - self.b
    
    # 乘法
    def mul(self):
        return self.a * self.b
    
    # 除法
    def div(self):
        return self.a / self.b

程序很简单,创建一个class Calculator类,通过__init__()方法接收两个参数,做int类型转换。创建add(),sub(),mul()和div()方法,分别进行加、减、乘和除运算运算。
根据上面实现的功能,创建test_calculator.py文件:

from calculator import Calculator

def test_add():
    c = Calculator(3, 5)
    result = c.sub()
    assert result == 8, '加法运算失败'

def test_sub():
    c = Calculator(7,2)
    result = c.sub()
    assert result == 5, '减法运算失败'
def test_mul():
    c = Calculator(3,3)
    result == c.mul
    assert reader == 10, '乘法运算失败'

def test_div():
    c = Calculator(6,2)
    result = c.div()
    assert result == 3, '除法运算失败'

if __name__ == '__main__'
    test_add()
    test_sub()
    test_mul()
    test_div()

在测试代码中,Calculator类首先引入到calculator文件中,然后初始化测试数据。然后调用类下的方法来获得计算结果,并断言结果是正确的。
这样的测试有几个问题。首先,需要自己定义断言失败的提示。接下来,如果测试函数运行失败,则后续的测试函数将不再运行。无法统计最后的执行结果。
当然,为了解决这些问题,可以编写更多的代码,但这偏离了我们单元测试的初衷。我们应该把重点放在测试本身上,而不是其他上面。引入单元测试框架可以很好地解决这些问题。
下面通过unittest单元测试框架重新编写测试用例:

import unittest
from calculator import Calculator

class TestCalculator(unittest.TestCase):
     
     def test_add(self):
        c = Calculator(3.5)
        result = c.add()
        self.assertEqual(result, 8)
     
     def test_sub(self):
        c = Calculator(7,2)
        result = c.sub()
        self.assertEqual(result,5)
        
     def test_mul(self):
        c = Calculator(3,3)
        result = c.mul
        self.assertEqual(result, 10)
        
     def test_div(self):
        c = Calculator(6,2)
        result = c.div()
        self.assertEqual(result,3)
        
if __name__== '__main__':
    unittest.main()

引入单元测试模块。在unittest中创建测试用例时,请务必遵守其“规则”。
(1)创建一个测试类,这里为TestCalculator类,必须要继承unittest模块的TestCase类。
(2)创建一个测试方法,该方法必须以“test”开头。
接下来的测试步骤与前面测试代码相同。
首先,调用被测试类,传入初始化数据。
其次,调用被测试方法,得到计算结果。通过unittest提供的assertEqual()方法来判断断言结果是否与预期结果相同。该方法由TestCase父类提供,由于继承了该类,所以可以通过self调用。
最后,调用unittest的main()函数来执行用例,它会按照前面的两条规则查找测试用例并执行。
测试结构明显丰富了很多,用"."表示一条运行通过的测试用例,用"F"表示一条运行失败的测试用例,用"E"表示一条运行错误的测试用例,用"s"表示一条运行跳过的测试用例。本次统计运行了4条测试用例,运行时间为0.001s,失败(failures)了1条测试用例。失败的测试用例也有了清晰说明。
2、重要概念
unittest文档中有四个重要的概念:测试用例(Test Case)、测试套件(Test Suite)、测试运行器(TestRunner)和测试夹具(Test Fixture)。只有理解了这些概念,才能理解单元测试的基本特征。
(1)Test Case
测试用例是最小的测试单元,用于检查特定输入集的特定返回值。Unittest提供了TestCase基类,我们创建的测试类需要继承这个基类,可以用来创建新的测试用例。
(2)Test Suite
测试套件是测试案例、测试套件或两者的集合,可用来组装要执行的测试集。Unisolatt提供TestSuite类以创建unittest套件。
(3)Test Runner
测试运行器(Test Runner)是一个用于协调测试执行并向用户提供结果的组件。测试运行程序可以使用图形界面、文本界面或返回特殊值来显示执行测试的结果。Unittest提供了运行测试用例的TextTestRunner类,并为示例生成HTML格式的测试报告。然后,将选择HTMLTestRunner来运行该类。
(4)Test Fixture
Test Fixture表示执行一个或多个测试所需的环境准备以及相关的清理操作。 例如,创建临时数据库、代理数据库和目录,或者启动服务器进程。 unittest提供了用于完成这些操作的方法,如setUp ( )/tearDown ( )和setUpClass ( )/tearDownClass ( )。
在理解了上面几个概念之后,我们对前面的测试用例做如下修改:

import unittest
from calculator import Calculator

class TestCalculator(unittest.TestCase):
    
    # 测试用例前置动作
    def setUp(self):
        print("test start")
    
    # 测试用例后置动作
    def searDown(self):
        print("test end")
    
    def test_add(self):
        c = Calculator(3.5)
        result = c.add()
        self.assertEqual(result, 8)
        
    def test_sub(self):
        c = Calculator(7, 2)
        result = c.sub()
        self.assertEqual(result, 10)
    def test_mul(self):
        c = Calculator(3, 3)
        result = c.mul()
        self.assertEqual(result, 3)
    
    def test_div(self):
        c = Calculator(6, 2)
        result = c.div()
        self.assertEqual(result, 3)
        
if __name__ = "__main__":
    # 创建测试条件
    suit = unittest.TestSuite()
    suit.addTest(TestCalculator("test_add"))
    suit.addTest(TestCalculator("test_sub"))
    suit.addTest(TestCalculator("test_mul"))
    suit.addTest(TestCalculator("test_div"))
    
    # 创建测试运行器
    runner = unittest.TextTestRunner()
    runner.run(suit)

首先,创建测试类并继承TestCase类。 在这个类下,创建以test开头的方法作为测试用例。上一期有说明,在这里再次说明是为了强调其重要性。
然后,向测试类添加了setUp ( )/tearDown ( )方法,用于定义测试用例的前置和后置行为。由于目前的测试暂时无法使用,因此在此定义了一些简单的打印。
接下来是测试用例的运行,在这里进行了很大的改变。 首先,我们没有调用TestSuite类下的addTest ( )来添加测试用例,而是放弃了unittest提供的main ( )方法。 由于一次只能添加一个用例,因此必须指定测试类和测试方法。 然后,调用TextTestRunner类下的run ( )来运行测试套件。
这样做确实比直接使用main()方法要麻烦得多,但也并不没有优点。
首先,测试用例的执行顺序可以由添加测试套件的顺序来决定,而main()方法只能根据测试类、方法的名称来执行测试用例。例如,TestA类与TestB类类似,而test_add()类比test_div()类先运行。
其次,当一个测试文件中有很多测试用例时,不需要每次都执行所有的测试用例,尤其是耗时的UI自动化测试。因此,可以通过测试套件和测试运行器灵活地控制要执行的测试用例。
从执行结果可以看到,setUp/tearDown作用于每条测试用例的开始之处与结束之处。
3、断言方法
在执行测试用例的过程中,通过将测试得到的实际结果与预期结果进行比较,得到最终的测试用例是否执行成功。unittest框架的TestCase类提供的测试结果断言方法如表所示。
TestCace 类提供的用于测试结果的断言方法

  方法                                                检查                  
  assertEqual(a,b)                                 a == b
  assertNoEqual(a,b)                               a !=b
  assertTrue(x)                                    bool(x) is True
  assertFalse(x)                                   bool(x) is False
  assertIs(a,b)                                    a is b
  assertIsNot(a,b)                                 x is not b
  assertIsNone(x)                                  x is None
  assertIsNotNone(x)                               x is not None
  assertIn(a,b)                                    a in b
  assertNotIn(a,b)                                 isinstance(a,b)
  assertNotIsInstance(a,b)                         not isinstance(a,b)

断言方法的使用如下所示:

import unittest

class TestAssert(unittest.TestCase):
    def test_equal(self):
        self.assertEqual(2+2 ,4)
        self.assertEqual("python", "python")
        self.assertNotEqual("hello", "python")
        
    def test_in(self):
        self.assertIn("hello", "hello world")
        self.assertNotIn("hi", "hello")
    
    def test_true(self):
        self.assertTrue(True)
        self.assertFalse(False)
        
if __name__ == "__main__":
    unittest.main()

运行上面的测试用例,即可通过测试结果推断出这些断言方法是如何使用的。
4、测试用例的组织与discover方法
前面针对Calculator类所编写的测试用例存在以下问题。
首先,一个功能显然不足以应对一个测试用例,写多少测试用例取决于对功能的需求和测试方法的理解。
接下来是测试用例的划分,一个测试类对应一个被测功能。

import unittest
from calculator import Calculator

class TestAdd(unittest.TestCase)
    """add()方法测试"""
    def test_add_integer(self):
        """整数相加测试"""
        c = Calculator
        self.assertEqual(a.add(), 8)
    
    def test_add_decimals(self):
        """小数相加测试"""
        c = Calculator(3.2, 5.5)
        self.assertEqual(c.add(), 8)
    
    def test_add_string(self):
        """字符串整数相加测试"""
        c = Calculator("7", "9")
        self.assertEqual(c.add(), 16)
        
    # ....
    
class TestSub(unittest.TestCase):
    """sub()方法测试"""
    pass
    # ....
    
if __name__ == "__main__":
    unittest.main()

一个测试文件可以定义多个测试类。但是,只要遵守测试用例的“规则”,main ( )方法就可以找到并执行它们。但是,要测试的类和方法可能有很多。
下面开发一个功能,用于判断某年是否为闰年。创建leap_year.py。

class LeapYear:
    """计算某年是否为闰年"""
    
    def __init__(self, year):
        self.year = int(year)
    
    def answer(self):
        year = self.year
        if year % 100 == 0:
            if year % 400 == 0:
                # 整百年能被400整除的是闰年
                return "{0}是闰年".format(year)
            
            else:
                return "{0}不是闰年".format(year)
        
        else:
            if year % 4 == 0:
                # 非整百年能被4整除的是闰年
                return "(0)是闰年".format(year)
            else:
                return "{0}不是闰年".format(year)

创建对应的测试文件test_leap_year.py。

import unittest
from leap_year import LeapYear

class TestLeapYear(unittest.TestCase):
    
    def test_200(self):
        ly = LeapYear(2000)
        self.assertEqual(ly.answer(), "2000是闰年")
        
    def test_2004(self):
        ly = LeapYear(2004)
        self.assertEqual(ly.answer(), "2004是闰年")
        
    def test_2017(self):
        ly = LeapYear(2017)
        self.assertEqual(ly.answer(), "2017不是闰年")
    
    def test_2100(self):
        ly = LeapYear(2100)
        self.assertEqual(ly.answer(), "2100不是闰年")
        
 
if __name__ == "__main__":
    unittest.main()

显然,闰年(leap_year.py)和计算器(calculator.py)的函数无关。它们的代码都写入这两个档案,因此最好将case、test_calculator.py和test_leap_year.py测试分开
如何运行多个测试文件? nuittest的TestLoadr类提供的discover ( )方法可以从多个文件中搜索测试用例。
类根据各种标准加载测试用例并将其返回到测试套件。 通常,不需要创建这个类的实例。 unittest提供了可以共享的defaultTestLoader类,可以使用该类的子类或方法创建实例。 discover ( )方法就是其中之一。

discover(start_dir,pattern='test*.py',top_lecel_dir=None)

将找到指定目录及其子目录的所有测试模块,并仅加载匹配的文件名。 如果启动的不是顶层目录,则必须分别指定顶层目录。
start_dir:待测试的模块名或测试用例目录
pattern=“test*.py”:测试用例文件名的匹配原则。此时匹配文件名以"test"开头的“.py”类型的文件,星号“*”表示任意多个字符。
top_level_dir=None:测试模块的顶层目录,如果没有顶层目录,则默认为None
现通过discover()方法重新实现run_tests.py文件的功能。

import unittest

# 定义测试用例的目录为当前目录中的test_case/目录
test_dir = './test_case'
suits = unittest.defaultTestLoader.discover(test_dir, pattern="test*.py")

if __name__ == '__main__':
    runner = unittest.TextTestRunner()
    runner.run(suits)

discover ( )方法根据测试用例的目录( test_dir )自动检索测试用例文件( test*.py ),并将检测到的测试用例添加到测试套件中,因此使用run (, 这种方法大大简化了测试用例的检索,需要根据文件匹配规则创建测试文件。
5、关于unittest还需要知道的
关于unittest还有一些问题值得进一步探讨,如测试用例的执行顺序等。
(1)测试用例的执行顺序
测试用例的运行顺序包括多个级别。多个测试目录>多个测试文件>多个测试类>多个测试方法(测试用例)。 unittes提供的main ( )和discover ( )方法按什么顺序搜索测试用例?
接下来,我们将运行一个示例,说明unittest的执行策略。

import unittest 
class TestBdd(unittest.TestCase)def setUp(self)print("test TestBdd")
    
    def test_ccc(self)print("test ccc")
        
    def test_aaa(self):
        print("test aaa")
        
class TestAdd(unittest.TestCase):
    
    def setUp(self):
        print("test TestAdd")
    
    def test_bbb(self)print("test bbb")
        
if __name__ == "__main__":
    unittest.main()

无论执行多少次,结果都是一样的。从上面的结果来看,相信你已经找到了main()方法执行测试用例的规律。
因为unittest默认根据ASCII码的顺序加载测试用例的(数字与字母的顺序为0-9,A-Z,a-z),所以TestAdd类会优先于TestBdd类被执行,test_aaa()方法会优先于test_ccc()方法被执行,也就是说,它并不是按照测试用例的创建顺序从上到下执行的。
discover ( )和main ( )方法的执行顺序相同。 上述规则也适用于测试目录和测试文件。 test_aaa.py文件将优先于test_bbb.py文件运行。 因此,想先执行某个测试文件时,可以进行命名上的控制。
除命名外,有没有其他办法控制测试用例的执行顺序呢?答案是肯定的,前面也有介绍,我们可以声明测试套件TestSuite类,通过addTest()方法按照一定的顺序来加载测试用例。
修改上面的例子如下:

#...
if __name__ == "__main__":
    # 构建测试集
    suite = unittest.TestSuite()
    suite.addTest(TestBdd("test_aaa"))
    suite.addTest(TestBdd("test_ccc"))
    suite.addTest(TestAdd("test_bbb"))
    
    # 执行测试
    runner = unittest.TextTestRunner()
    runner.run(suite)

执行顺序与addTest()方法加载测试用例的顺序相同。但是,如果测试案例非常长,并且先前已说明执行测试套件的最佳方式是命名检查顺序,则不建议使用此方法。如果房屋测试在设计时不是相互依赖的,则房屋测试的执行顺序并不重要。
(2)执行多级目录的测试用例
当测试用例的数量达到一定量级时,就要考虑目录换分,比如规划如下测试目录。
对于上面的目录结构,如果将discover()方法中的start_dir参数定义为"./test_case"目录,那么只能加载test_a.py文件中的测试用例。如何让unittest查找test_case/下子目录中的测试文件呢?方法很简单,就是在每个子目录下放一个__init__.py文件。init.py文件的作用是将一个目录标记成一个标准的Python模块。
在运行一个测试时,有时需要直接跳过一些测试用例,或者在测试用例满足一定条件时跳过测试,或者直接将测试用例设置为失败。Unittest提供了一个装饰器来满足这些需求。
无条件地跳过装饰的测试,需要说明跳过的原因:unittest.skip(reason)
如果条件为真,则跳过装饰的测试:unittest.skipIf(condition,reason)
当条件为真时,执行装饰的测试:unittest.skipUnless(condition,reason)
不管执行结果是否失败,都将测试标记为失败:unittest.expectedFailure()

import unittest

class MyTest(unittest.TestCase):
    @unittest.skip("直接跳过测试")
    def test_skip(self):
        print("test_aaa")
    
    @unittest.skipIf(3 > 2,"当条件为真时跳过测试")
    def test_skip_if(self):
        print('test_bbb')
        
    @unittest.skipUnless(3 > 2,"当条件为真时执行测试")
    def test_skip_unless(self):
        print("test_ccc")
    
    @unittest.expectedFailure
    def test_expected_failure(self):
        self.assertEqual(2, 3)
        
if __name__ == "__main__":
    unittest.main()

上面的例子创建了四条测试用例
第一条测试用例通过@unittest.skip()装饰,直接跳过测试
第二条测试用例通过@unittest.skipIf()装饰,当条件为真时跳过测试;3 > 2 条件为真(True),所以跳过测试。
第三条测试用例通过@unittest.skipUnless()装饰,当条件为真时执行测试;3 > 2条件为真(True),执行测试。
第四条测试用例通过@unittest.expectedFailure装饰,不管执行结果是否失败,都将测试标记为失败,但不会抛出失败信息。
当然,这些方法同样适用于测试类,只需要它们针对测试类装饰即可。

import unittest

@unittest.skip("直接跳过,不测试该测试类")
class MyTest(unittest.TestCase):
# ...

(3)Fixture
可以把Fixture看成是三明治饼干外面的两块饼干。这两片饼干是setUp/tearDown,中间的奶油是测试用例。此外,unittest还提供了更大范围的Fixture

import unittest

def setUpModule():
    print("test module start >>>>>>>>>>>>>")

def tearDownModuke():
    print("test module end >>>>>>>>>>>")
    
class MyTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        print("test class start =======>")
    
    @classmethod
    def tearDownClass(cls):
        print("test class end ========>")
    
    def setUp(self):
        print("test case start -->")
    
    def tearDown(self):
        print("test case end -->")
    
    def test_casel(self):
        print("test case1")
    
    def test_case2(self):
        print("test case2")
        
if __name__ == "__main__":
    unittest.main()

在整个模块的开始与结束时被执行:setUpModule/tearDownModule
在测试类的开始与结束时被执行:setUpClass/tearDownClass
在测试用例的开始与结束时被执行:setUp/tearDown
需要注意的是,setUpClass/tearDownClass为类方法,需要通过@classmethod进行装饰;另外,方法的参数为cls。其实,cls与self并没有什么本质区别,都只表示方法的第一个参数。
思维导图
在这里插入图片描述

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值