unittest 单元测试框架

我在讲解 Appium 的时候有一篇文章使用到了unittest单元测试框架,从那篇文章就可以看出来这个框架给测试带来的便利。今天做一次比较全面系统的介绍。配以大量的脚本示例,希望可以帮助到更多的朋友。

单元测试就是用一个函数(方法)去验证另一个函数(方法)是否正确。

unittest单元测试框架主要完成以下三件事:

提供用例组织与执行:当测试用例只有几条的时候可以不考虑用例的组织,但是当测试用例数量较多时,此时就需要考虑用例的规范与组织问题了。unittest单元测试框架就是用来解决这个问题的。

提供丰富的比较方法:既然是测试,就有一个预期结果和实际结果的比较问题。比较就是通过断言来实现,unittest单元测试框架提供了丰富的断言方法。

提供丰富的日志:每一个失败用例我们都希望知道失败的原因,所有用例执行结束我们有希望知道整体执行情况,比如总体执行时间,失败用例数,成功用例数。unittest单元测试框架为我们提供了这些数据。

比如我们有一个方法是计算两数之和,我们输入几组数值求和,如果结果与预期结果5相同,那我们就认为这个结果是我们想要的:


import unittest


class Count(object):
    def __init__(self,a,b):
        self.a = a
        self.b = b


# 计算加法
    def add(self):
        return self.a + self.b


# 单元测试,测试 add()方法
class TestCount(unittest.TestCase):

    def setUp(self):
        print("Test Start")

    def test_add(self):
        # 这里参数3,4是我们写死的,实际是可能变化不固定的。比如将读取到的表格内的数据当做这两个参数。
        s = Count(3,4)
        # 这里的比较对象5就是我们的预期结果,与之不同即为 Fail。
        self.assertEqual(s.add(),5)

    def tearDown(self):
        print("Test end")

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

这里我们将待测方法和单元测试写在了一个.py文件里,这样对于日后想要修改带来一定的不便,所以我们常把待测方法和单元测试分开写。

待测试方法:

calculator.py

import unittest

class Count(object):
    def __init__(self,a,b):
        self.a = a
        self.b = b


# 计算加法
    def add(self):
        return self.a + self.b

单元测试:
test.py

from calculator import Count
# 单元测试必须要引入unittest模块
import unittest
# 测试方法必须要继承自unittest.TestCase
class TestCount(unittest.TestCase):

    def setUp(self):
        print("Test Start测试开始")

    def test_add(self):
        s = Count(3,4)
        self.assertEqual(s.add(),5)

    def tearDown(self):
        print("Test end测试结束")

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

注意:count.py 和 test.py 要在同一路径下才能 import。

脚本执行结果报告

这里要特别说明一点,测试方法必须要以“test_”开头,否则unittest.TestCase不识别,无法进行验证。如果测试方法没有以“test_”开头,则会报错,比如我们将方法名 test_add改为 testadd,执行结果如下:


错误的测试方法命名

补充说明:一个.py文件有两种使用方式:直接使用和模块调用。上面的calculator.py文件就是被模块调用,test.py 就是直接使用。脚本最后的 if 语句

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

表示当前文件只能直接使用,不能模块调用。在这里把这两行代码去掉也不会影响运行结果。

单元测试的重要概念

1. TestCase
一个TestCase的实例就是一个测试用例。一个测试用例要包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown)。一个测试用例是一个完整的测试单元,通过运行这个测试单元,可以对某一个功能进行验证。

2. TestSuite

对于某一个功能模块的验证可能需要多个测试用例,多个测试用例集合在一起执行验证某一个功能,这样就是一个TestSuite。通过addTest()方法将 TestCase 加载到 TestSuite()中。

3. TestRunner
TestRunner可以使用图形界面、文本界面,或返回一个特殊的值等方式来表示测试执行的结果。TextTestRunner提供的 run()方法来执行 test suite/test case。

4.TestFixture
一个测试用例环境的搭建和销毁就是一个 fixture。

下面我们举例说明,对上面的 test.py 文件进行修改:

在实际操作这个示例的时候遇到一些问题,晚些时候补充!
断言方法
方法 检查
assertEqual(a,b) a==b
assertNotEqual(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) a is not b
assertIn(a,b) a in b
assertNotIn(a,b) a not in b

举例说明断言的用法:
count.py

def is_prime(n):
    if n <= 1:
        return False
    for i in range(2, n):
        if n % i == 0:

            print("i ====哈啊哈哈哈=== %s,%s" % (i, n))
            print(len(range(2, n)))

            return False
        print("i ====哈啊哈哈wo哈=== %s,%s" % (i, n))
        print(len(range(2, n)))
        return True

test2.py

from count import is_prime
import unittest


class Test(unittest.TestCase):
    def setUp(self):
        print("测试开始")

    def tearDown(self):
        print("测试结束")

    def test_case(self):
        self.assertTrue(is_prime(9),msg="is Not Prime")
        


if __name__ == "__main__":
    unittest.main()
组织单元测试用例

当我们新增被测功能和相应的测试用例后,再来看看 unittest 是如何扩展和组织新增的测试用例的。
我们现在对calculator.py文件新增一个函数:
calculator.py

import unittest


class Count(object):
    def __init__(self,a,b):
        self.a = a
        self.b = b


# 计算加法
    def add(self):
        return self.a + self.b

# 计算减法
    def sub(self):
        return self.a - self.b

接下来再修改test.py文件:

from calculator import Count
import unittest


class TestCount(unittest.TestCase):

    def setUp(self):
        print("测试开始")

    def tearDown(self):
        print("测试结束")

    def test_add(self):
        s = Count(2,1)
        self.assertEqual(s.add(), 3)
    print("用例1")

    def test_add2(self):
        s = Count(2,3)
        self.assertEqual(s.add(), 5)
    print("用例2")


class TestSub(unittest.TestCase):
    def setUp(self):
        print("测试减法开始")

    def tearDown(self):
        print("测试减法结束")

    def test_sub(self):
        s = Count(4, 1)
        self.assertEqual(s.sub(), 3, msg="4 - 1 != 2")

    def test_sub2(self):
        s = Count(3, 1)
        self.assertEqual(s.sub(), 2)


if __name__ == '__main__':
        print("到这了")
        #  构造测试集
        suite = unittest.TestSuite()
        suite.addTest(TestCount.test_add())
        suite.addTest(TestCount.test_add2())
        suite.addTest(TestSub.test_sub())
        suite.addTest(TestSub.test_sub2())
        # 执行测试
        runner = unittest.TextTestRunner()
        runner.run(suite)

这个脚本执行的结果是四个函数全都 pass:


76716464-E157-405B-B3F7-739E8EBDF56F.png

现在我们对测试脚本稍加修改,使得结果有成功有失败:
test.py

from calculator import Count
import unittest


class TestCount(unittest.TestCase):

    def setUp(self):
        print("测试开始")

    def tearDown(self):
        print("测试结束")

    def test_add(self):
        s = Count(2,1)
        self.assertEqual(s.add(), 3)
    print("用例1")

    def test_add2(self):
        s = Count(2,3)
        self.assertEqual(s.add(), 15)
    print("用例2")


class TestSub(unittest.TestCase):
    def setUp(self):
        print("测试减法开始")

    def tearDown(self):
        print("测试减法结束")

    def test_sub(self):
        s = Count(4, 1)
        self.assertEqual(s.sub(), 2, msg="错误原因:4 - 1 != 2")

    def test_sub2(self):
        s = Count(3, 1)
        self.assertEqual(s.sub(), 2)


if __name__ == '__main__':
        print("到这了")
        #  构造测试集
        suite = unittest.TestSuite()
        suite.addTest(TestCount.test_add())
        suite.addTest(TestCount.test_add2())
        suite.addTest(TestSub.test_sub())
        suite.addTest(TestSub.test_sub2())
        # 执行测试
        runner = unittest.TextTestRunner()
        runner.run(suite)

执行结果如下:


B6EC4B4D-198A-4B29-8379-DF20688C7BDA.png

从结果我们能够看出来,一共4个用例,其中2个测试通过,2个未能通过,未通过的函数和原因都已经列出。
通过观察上面的脚本还是有重复代码,我们可以继续修改:

from calculator import Count
import unittest


class MyTest(unittest.TestCase):

    def setUp(self):
        print("测试开始")

    def tearDown(self):
        print("测试结束")


class TestCount(MyTest):

    def test_add(self):
        s = Count(2,1)
        self.assertEqual(s.add(), 3)
    print("用例1")

    def test_add2(self):
        s = Count(2,3)
        self.assertEqual(s.add(), 5)
    print("用例2")


class TestSub(MyTest):
    def setUp(self):
        print("测试减法开始")

    def test_sub(self):
        s = Count(4, 1)
        self.assertEqual(s.sub(), 3, msg="错误原因:4 - 1 != 2")

    def test_sub2(self):
        s = Count(3, 1)
        self.assertEqual(s.sub(), 2)


if __name__ == '__main__':
        print("到这了")
        #  构造测试集
        suite = unittest.TestSuite()
        suite.addTest(TestCount.test_add())
        suite.addTest(TestCount.test_add2())
        suite.addTest(TestSub.test_sub())
        suite.addTest(TestSub.test_sub2())
        # 执行测试
        runner = unittest.TextTestRunner()
        runner.run(suite)

上面脚本类test_add和TestSub都继承 MyTest,而 MyTest 又继承unittest.TestCase,所以这两个类也就继承了unittest.TestCase。这样封装的前提是,这两个类都必须在 setUp 和 tearDown 中干的事情是一样的。

我在写这些脚本的时候曾思考过一个问题。就拿我们公司目前已有的测试用例(已经有接近400个)来说,如果都写在 test.py 一个文件里,那这个文件该有多冗余,日后维护起来该何等麻烦,有没有一个更好的办法来组织这些测试用例呢?别说,还真有!其实接下来要介绍的内容在我介绍 Appium 的时候已经使用该方法组织用例了。
首先我们分析一下上面的 test.py 文件不好在哪里。add()和 sub()两个方法分别实现两个不同的功能,为了验证这两个方法,那就需要两个类来实现,如果有更多的功能需要验证,那就需要更多的类,所以我们把这些类都拆分开。每一个函数(方法)作为一个单元测试文件。

testadd.py

from calculator import Count
import unittest


class TestAdd(unittest.TestCase):

   def setUp(self):
       print()

   def tearDown(self):
       print()

   def test_add(self):
       # Count类有两个参数,创建对象的时候要有两个参数。
       s = Count(2,4)
       self.assertEqual(s.add(), 6, msg="实际结果和预期不符")

   def test_add2(self):
       s = Count(1, 4)
       self.assertEqual(s.add(), 5)

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

testsub.py

from calculator import Count
import unittest


class TestSub(unittest.TestCase):

    def setUp(self):
        print()

    def tearDown(self):
        print()

    def test_sub(self):
        # Count类有两个参数,创建对象的时候要有两个参数。
        s = Count(10,4)
        self.assertEqual(s.sub(), 5, msg="实际结果和预期不符")

    def test_sub2(self):
        s = Count(19, 14)
        self.assertEqual(s.sub(), 5)

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

如果只是这样做改变,那么我们验证如果需要验证 add()和 sub()两个方法,就需要分两次执行 testadd.py和 testsub.py 两个文件。现在我们继续组织这两个脚本,使得可以一次执行这两个文件。

test.py

import unittest
# 导入测试文件
import testadd, testsub

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

suite.addTest(testadd.TestAdd("test_add"))
suite.addTest(testadd.TestAdd("test_add2"))

suite.addTest(testsub.TestSub("test_sub"))
suite.addTest(testsub.TestSub("test_sub2"))

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

上面这种组织用例的方法要比之前简洁一些,但是当用例更多的时候,我们还需要通过 addTest()方法将新增的用例添加到 test.py 文件中。我们讲解一个可以自动添加的方法。这就是 TestLoader 类中提供的一个 discover()方法。

TestLoader 负责根据各种标准加载测试用例,并将他们返回给测试套件。正常情况下,不需要创建这个类的实例。unittest 提供了可以共享的 defaultTestLoader 类,可以使用其子类和方法创建实例,discover()方法就是这个类中的一个方法之一。

test.py

import unittest

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

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

这次的test.py可能就是 终极组织用例的方法了。现在我们介绍一下 discover()方法中参数的意思:

  • test_dir:需要加载的单元测试文件的路径。因为我这里 test.py文件和和各个测试用例在同一路径下,所以test_dir = './'。如果不是在同一路径下,就填写绝对路径,比如我的路径就应该是test_dir = /Users/guxuecheng/Desktop/unittest脚本

  • patten 是一个正则表达式,pattern='test*.py'是指加载所有 test 开头的.py 文件,*表示任意多个字符。

discover()方法会自动的根据测试目录(test_dir)匹配查找测试用例文件(test*.py),并将查找到的测试用例组装到测试套件 suite 中,因此可以通过run()方法执行 discover。

粗暴的解释一下最后一段代码的意思:

通俗的理解name == 'main':假如你叫小明.py,在朋友眼中,你是小明(name == '小明');在你自己眼中,你是你自己(name == 'main')。

if name == 'main'的意思是:当.py文件被直接运行时,if name == 'main'之下的代码块将被运行;当.py文件以模块形式被导入时,if name == 'main'之下的代码块不被运行。

  




在网上看到了一个博主的文章,给我提供了一个新的思路,感觉很不错,在此感谢博主灰蓝,将文章搬到这篇文章里,方便日后翻阅:
先准备一些待测方法,这些方法没有组织到一个类里,也没有初始化参数,很简练:
mathfunc.py

def add(a, b):
    return a+b

def minus(a, b):
    return a-b

def multi(a, b):
    return a*b

def divide(a, b):
    return a/b

接下来测试这些方法:
test_mathfunc.py

# -*- coding: utf-8 -*-

import unittest
from mathfunc import *


class TestMathFunc(unittest.TestCase):
    """Test mathfuc.py"""

    def test_add(self):
        """Test method add(a, b)"""
        self.assertEqual(3, add(1, 2))
        self.assertNotEqual(3, add(2, 2))

    def test_minus(self):
        """Test method minus(a, b)"""
        self.assertEqual(1, minus(3, 2))

    def test_multi(self):
        """Test method multi(a, b)"""
        self.assertEqual(6, multi(2, 3))

    def test_divide(self):
        """Test method divide(a, b)"""
        self.assertEqual(2, divide(6, 3))
        self.assertEqual(2.5, divide(5, 2))

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

组织 TestSuite


上面的代码示例了如何编写一个简单的测试,但有两个问题,我们怎么控制用例执行的顺序呢?(这里的示例中的几个测试方法并没有一定关系,但之后你写的用例可能会有先后关系,需要先执行方法A,再执行方法B),我们就要用到TestSuite了。我们添加到TestSuite中的case是会按照添加的顺序执行的。

问题二是我们现在只有一个测试文件,我们直接执行该文件即可,但如果有多个测试文件,怎么进行组织,总不能一个个文件执行吧,答案也在TestSuite中。

下面来个例子:

在文件夹中我们再新建一个文件,test_suite.py:

# -*- coding: utf-8 -*-

import unittest
from test_mathfunc import TestMathFunc

if __name__ == '__main__':
    suite = unittest.TestSuite()

    tests = [TestMathFunc("test_add"), TestMathFunc("test_minus"), TestMathFunc("test_divide")]
    suite.addTests(tests)

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

将结果输出到文件中


用例组织好了,但结果只能输出到控制台,这样没有办法查看之前的执行记录,我们想将结果输出到文件。很简单,看示例:

修改test_suite.py:

# -*- coding: utf-8 -*-

import unittest
from test_mathfunc import TestMathFunc

if __name__ == '__main__':
    suite = unittest.TestSuite()
    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))

    with open('UnittestTextReport.txt', 'a') as f:
        runner = unittest.TextTestRunner(stream=f, verbosity=2)
        runner.run(suite)

执行此文件,可以看到,在同目录下生成了UnittestTextReport.txt,所有的执行报告均输出到了此文件中,这下我们便有了txt格式的测试报告了。

进阶——用HTMLTestRunner输出漂亮的HTML报告


我们能够输出txt格式的文本执行报告了,但是文本报告太过简陋,是不是想要更加高大上的HTML报告?但unittest自己可没有带HTML报告,我们只能求助于外部的库了。

官方原版:http://tungwaiyip.info/software/HTMLTestRunner.html
我修改后的: https://pan.baidu.com/s/1kV64YZ9

修改我们的 test_suite.py:

# -*- coding: utf-8 -*-

import unittest
from test_mathfunc import TestMathFunc
from HtmlTestRunner import HTMLTestRunner

if __name__ == '__main__':
    suite = unittest.TestSuite()
    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))

    with open('HTMLReport.html','w') as f:
        runner = HTMLTestRunner(stream=f,
                                report_title='Test Report',
                                descriptions='Report Details',
                                verbosity=2,
                                output='report',

                                )
        runner.run(suite)
HTML 测试报告

总结一下:


  1. unittest是Python自带的单元测试框架,我们可以用其来作为我们自动化测试框架的用例组织执行框架。
  2. unittest的流程:写好TestCase,然后由TestLoader加载TestCase到TestSuite,然后由TextTestRunner来运行TestSuite,运行的结果保存在TextTestResult中,我们通过命令行或者unittest.main()执行时,main会调用TextTestRunner中的run来执行,或者我们可以直接通过TextTestRunner来执行用例。
  3. 一个class继承unittest.TestCase即是一个TestCase,其中以 test 开头的方法在load时被加载为一个真正的TestCase。
  4. verbosity参数可以控制执行结果的输出,0 是简单报告、1 是一般报告、2 是详细报告。
  5. 可以通过addTest和addTests向suite中添加case或suite,可以用TestLoader的loadTestsFrom__()方法。
  6. 用 setUp()、tearDown()、setUpClass()以及 tearDownClass()可以在用例执行前布置环境,以及在用例执行后清理环境
  7. 我们可以通过skip,skipIf,skipUnless装饰器跳过某个case,或者用TestCase.skipTest方法。
  8. 参数中加stream,可以将报告输出到文件:可以用TextTestRunner输出txt报告,以及可以用HTMLTestRunner输出html报告。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容