• 一篇文章了解_unittest


    1. 基本概念

    2018年10月7日 星期日

    11:39

    unittest是python自带的单元测试框架,有时候又被称为”PyUnit”,是python版本的JUint实现。

    该框架的作者是 Kent Beck和Erich Gamma,感谢祖师爷赏饭吃。

    在学习使用unittest库之前,我们需要了解一下unittest库的一些重要概念:

    • test fixture: 代表了用例执行前的准备工作和用例执行之后的清理工作。比如在用例执行前创建临时文件和文件夹,又或者启动1个server进程等;
    • test case: 测试用例,这个相信大家都不陌生。是测试的最小单位,一般检查一组输入的响应(输出)是否符合预期。unittest模块提供了TestCase类来帮助我们创建测试用例;
    • test suite: 经常被翻译成”测试套件”,也有人称为”测试套”,是测试用例或测试套件的集合,一般用来把需要一起执行的用例组合到一起;
    • test runner: 用来执行测试用例并输出测试结果的组件。可以是图形界面或命令行界面;

    总之

    • test fixture的功能可以理解成是初始化和清理测试数据及环境
    • test case是测试用例
    • test suite是用例集合
    • test runner的作用是运行用例并返回结果

    2. 基本用法

    2018年10月7日 星期日

    11:40

    我们通过最简单的例子来看一下unittest的基本用法,下面的代码测试了3个python字符串方法,基本上满足了大部分情况下的测试需求

    import unittest

    class TestStringMethods(unittest.TestCase):

    def test_upper(self):
            self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
            self.assertTrue('FOO'.isupper())
            self.assertFalse('Foo'.isupper())

    def test_split(self):
            s = 'hello world'
            self.assertEqual(s.split(), ['hello', 'world'])
            # check that s.split fails when the separator is not a string
            with self.assertRaises(TypeError):
                s.split(2)

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

    解释一下关键点

    • 可以通过继承unittest.TestCase类来定义我们自己的测试用例,1个测试用例类下面可以有多个测试方法(test)或者叫做测试点
    • 记住这个套路:测试用例中方法名以test开头的方法才是测试方法,比如上面的例子里定义了3个以test开头的方法,分别是test_upper,test_isupper和test_split。非测试方法是不会被test runner执行的
    • 断言是测试用例的核心。我们使用assertEqual()来判断预期结果,用assertTrue()assertFalse来做是非判断,以及用assertRaises()来判断预期的异常是否有被抛出。这些unittest提供的以assert开头的方法就是断言,一般情况下,每个测试方法里都必须有断言
    • 最后, unittest.main提供了最简单的运行用例的方式。当我们从命令行运行上面的代码时,我们可以看到如下的输出

    ...
    ----------------------------------------------------------------------
    Ran 3 tests in 0.000s

    OK

    除了使用unittest.main,还有其他的方式可以运行测试用例,比如把最后2行替换为

    suite = unittest.TestLoader().loadTestsFromTestCase(TestStringMethods)
    unittest.TextTestRunner(verbosity=2).run(suite)

    运行用例,结果将会如下所示

    test_isupper (__main__.TestStringMethods) ... ok
    test_split (__main__.TestStringMethods) ... ok
    test_upper (__main__.TestStringMethods) ... ok

    ----------------------------------------------------------------------
    Ran 3 tests in 0.001s

    OK

    3. 实例: 测试弱密码

    2018年10月7日 星期日

    11:41

    背景

    考虑这样一个测试弱密码的实例,这个我们在pytest相关教程中也有过描述。

    我们需要判断用户的密码中包含简单密码,规则是这样的,密码必须至少6位,满足6位的话判断用户的密码不是password123或者password之类的弱密码。

    对于如下的测试数据,我们要如何使用unittest来进行相关测试呢?

    [
      {"name":"jack","password":"Iloverose"},
      {"name":"rose","password":"Ilovejack"},
      {"name":"tom","password":"password123"}
    ]

    Test fixture

    前文我们也说过,text fixture的主要功能是初始化测试数据或环境以及清理测试数据或环境。

    考虑上面的例子,对我们而已,在用例执行之前初始化上面的测试数据是有必要的,我们可以把上面的数据用python的数据结构来表示,比较合适的数据结构是python的字典。这样做有如下的好处

    • 统一初始化一些需要在多个用例之间共享的数据
    • 可以在初始化的时候做一些数据的处理工作,比如过滤一些无效数据等

    Test fixture最简单的实现方式是通过自定义下面的2个方法:

    • TestCase.setUp方法在每个测试方法运行之前都会运行一次,适合为每个用例都初始化一遍数据
    • TestCase.tearDown方法在每个测试方法运行之后都会运行一次,适合为每个用例都清理一遍数据

    代码

    新建名为test_password_1.py的文本文件,输入如下内容

    import unittest

    class PasswordTeseCase(unittest.TestCase):

    def setUp(self):
            print('set up')
            self.test_data = [
                dict(name='jack', password='Iloverose'),
                dict(name='rose', password='Ilovejack'),
                dict(name='tom', password='password123')
            ]

    def test_week_password(self):
            for data in self.test_data:
                passwd = data['password']

    self.assertTrue(len(passwd) >= 6)

    msg = "user %s has a weak password" %(data['name'])
                self.assertTrue(passwd != 'password', msg)
                self.assertTrue(passwd != 'password123', msg)

    def test_dummy(self):
            pass

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

    运行

    在命令行里输入 python test_password_1.py来运行用例,结果如下

    $ python test_password_1.py
    set up
    .set up
    F
    ======================================================================
    FAIL: test_week_password (__main__.PasswordTeseCase)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "test_password_1.py", line 21, in test_week_password
        self.assertTrue(passwd != 'password123', msg)
    AssertionError: False is not true : user tom has a weak password

    ----------------------------------
    Ran 2 tests in 0.001s

    FAILED (failures=1)

    解释一下

    • setUp方法运行了2次,所以打印出了2次’set up’,这是因为上面的用例中有2个测试方法(2个方法名以test开头的方法),setUp会在每个测试方法执行之前执行1次
    • 由于用户tom的密码是弱密码password123,所以上面的用例运行失败了,打印出1个”F”,有几个F就代表有几个测试用例失败
    • 测试方法运行失败时测试结果里会打印出失败方法的方法名,因此好的测试方法名可以方便我们快速找出失败用例
    • 为了让错误信息更加容易理解,我们经常会自定义断言出错提示消息,比如msg = "user %s has a weak password" %(data['name'])。一旦断言失败,我们一眼就能看出是哪个用户的密码强度不够

    亲自动手试一试

    假设我们增加1条测试数据,如下所示

    [
      {"name":"jack","password":"Iloverose"},
      {"name":"rose","password":"Ilovejack"},
      {"name":"tom","password":"password123"},
      {"name":"jerry","password":"password"}
    ]

    再运行上面的用例,观察一下测试结果是否会有不同?如果没有不同,那是为什么?

    4. 实例: 读取测试数据并测试弱密码

    2018年10月7日 星期日

    11:42

    背景

    接上一节的弱密码例子,我们的用例尽管运行的不错,但还是有点问题。

    假如我们需要增加一些测试数据,那么我们就必须去修改setUp方法,在test_data列表中增加数据,频繁修改代码以适应一些不变的测试场景,这是没有必要的开销,可以想办法去优化。

    我们可以把测试数据保存在文件里,通过读取文件的方式,每次动态从测试用例读取数据,这样数据的改变并不会影响用例,用例逻辑相对稳定,维护成本得到一定的降低。

    设计测试数据

    我们可以把测试数据保存成json格式,json格式的数据在各个语言间有较好的通用性,比较适合复用。

    新建user_data.json文件,内容如下

    [
      {"name":"jack","password":"Iloverose"},
      {"name":"rose","password":"Ilovejack"},
      {"name":"tom","password":"password123"}
    ]

    使用python的json库解析上面的json文件,可以得到如上节中test_data一致的数据。

    代码

    新建test_password_2.py,内容如下

    import unittest
    import json

    class PasswordWithJsonTeseCase(unittest.TestCase):
        data_file_path = './user_data.json'

    def setUp(self):
            print('set up')
            self.test_data = json.loads(open(self.data_file_path).read())

    def test_week_password(self):
            for data in self.test_data:
                passwd = data['password']

    self.assertTrue(len(passwd) >= 6)

    msg = "user %s has a weak password" %(data['name'])
                self.assertTrue(passwd != 'password', msg)
                self.assertTrue(passwd != 'password123', msg)

    def test_dummy(self):
            pass

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

    跟上一节相比,最大的不同点是现在test_data通过解析json文件的方式来赋值self.test_data = json.loads(open(self.data_file_path).read())。

    执行测试文件,结果应该与上一节一致。

    发现问题

    上面的代码有2个测试方法:test_week_password和test_dummy。由于setUp会在每个测试方法执行之前执行一次,那么setUp方法会执行2次,相应的json文件也会读取2次。如果测试方法多的话,那么反复读取json文件对性能来说是一个巨大的挑战。

    优化

    对于上面的测试数据读取场景,我们可以在所有测试方法执行前读取一次数据,毕竟测试数据在所有测试方法执行过程中是保持不变的。

    setUpClass()和tearDownClass()

    • setUpClass方法在每个测试用例类执行之前会执行一次,接收该class作为唯一的参数,并且必须使用装饰器classmethod()
    • tearDownClass: 在所有测试方法执行完之后被调用1次,调用方式跟上面的方法类似

    重构

    下面我们重构代码以达到只读取1次测试数据的目的,新建文件`test_password_3.py,内容如下

    import unittest
    import json

    class WeakPasswordTeseCase(unittest.TestCase):

    @classmethod
        def setUpClass(kls):
            data_file_path = './user_data.json'
            print('before all test methods')
            with open(data_file_path) as f:
                kls.test_data = json.loads(f.read())

    def test_week_password(self):
            for data in self.test_data:
                passwd = data['password']

    self.assertTrue(len(passwd) >= 6)

    msg = "user %s has a weak password" %(data['name'])
                self.assertTrue(passwd != 'password', msg)
                self.assertTrue(passwd != 'password123', msg)

    def test_dummy(self):
            pass

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

    有几点需要提及一下

    • 使用open 方法的with模式可以在读取文件后自动关闭文件
    • 在setUpClass方法中可以直接设置变量,比如kls.test_data = json.loads(f.read()),在其他测试方法中可以被访问

    5. 实例: 找出所有是弱密码的用户

    2018年10月7日 星期日

    11:43

    背景

    当我们的测试数据是下面这些的时候,我们的用例是有问题的。

    [
      {"name":"jack","password":"Iloverose"},
      {"name":"rose","password":"Ilovejack"},
      {"name":"tom","password":"password123"},
      {"name":"jerry","password":"password"}
    ]

    我们的用例只能找出tom是弱密码的用户,jerry这个用户会成为漏网之鱼。

    为什么

    这是因为在unittest中,一旦某个测试方法中的断言失败,后续的断言都不会被执行。

    还原一下上面的例子,当用例在断言tom失败后,for循环就退出了,测试方法也执行完毕了,后面jerry这条数据就不会被断言。

    怎么办

    我们可以重构一下我们的用例,让整个用例只断言1次,断言失败以后就把弱密码的用户打印出来。

    代码

    修改user_data.json文件,加入一些测试数据,修改后的user_data.json文件应该是

    [
      {"name":"jack","password":"Iloverose"},
      {"name":"rose","password":"Ilovejack"},
      {"name":"tom","password":"password123"},
      {"name":"jerry","password":"password"},
      {"name":"fred","password":"123456"},
      {"name":"elma","password":"654321"}
    ]

    新建test_password_4.py文件,内容如下


    import unittest
    import json

    class WeakPasswordTeseCase1(unittest.TestCase):

    @classmethod
        def setUpClass(kls):
            data_file_path = './user_data.json'
            print('before all test methods')
            with open(data_file_path) as f:
                kls.test_data = json.loads(f.read())

    def test_week_password(self):
            res = True
            msg = []
            for data in self.test_data:
                passwd = data['password']
                tmp_res = True

    tmp_res = tmp_res and (len(passwd) >= 6)
                tmp_res = tmp_res and (passwd != 'password')
                tmp_res = tmp_res and (passwd != 'password123')
                if not tmp_res:
                    msg.append("user %s has a weak password %s" %(data['name'], data['password']))
                res = res and tmp_res

    self.assertTrue(res, " ".join(msg))

    def test_dummy(self):
            pass

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

    运行及结果

    在命令行中运行python test_password_4.py,结果如下

    $ python test_password_4.py
    before all test methods
    .F
    ======================================================================
    FAIL: test_week_password (__main__.WeakPasswordTeseCase1)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "test_password_4.py", line 27, in test_week_password
        self.assertTrue(res, " ".join(msg))
    AssertionError: user tom has a weak password password123
    user jerry has a weak password password

    ----------------------------------------------------------------------
    Ran 2 tests in 0.001s

    FAILED (failures=1)

    我们能学到什么

    • 断言一旦失败之后测试方法就会结束运行,所以一般来说1个测试方法推荐只有1个断言
    • 如果一个测试方法里面必须要有多个断言,那么要确保前面的断言失败之后,后面的断言就算不运行也不会影响测试的范围和结果
    • for循环中的断言一旦失败,for循环就退出了
    • 上面演示的测试用例写法其实具备了一定的数据驱动测试的思想

    6. 命令行接口

    2018年10月7日 星期日

    11:44

    背景

    unittest支持命令行接口,我们可以在命令行里指定运行具体的测试用例。

    实例

    在test_password_1.py中定义了PasswordTeseCase用例,我们可以从命令行中指定只运行该用例。

    $ python -m unittest test_password_1.PasswordTeseCase
    set up
    .set up
    F
    ======================================================================
    FAIL: test_week_password (test_password_1.PasswordTeseCase)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/Users/easonhan/code/testclass.net/src/pyunit/test_password_1.py", line 21, in test_week_password
        self.assertTrue(passwd != 'password123', msg)
    AssertionError: False is not true : user tom has a weak password

    ----------------------------------------------------------------------
    Ran 2 tests in 0.001s

    FAILED (failures=1)

    还可以使用-v参数来获得更详细的输出

    $ python -m unittest test_password_1.PasswordTeseCase -v
    test_dummy (test_password_1.PasswordTeseCase) ... set up
    ok
    test_week_password (test_password_1.PasswordTeseCase) ... set up
    FAIL

    ======================================================================
    FAIL: test_week_password (test_password_1.PasswordTeseCase)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/Users/easonhan/code/testclass.net/src/pyunit/test_password_1.py", line 21, in test_week_password
        self.assertTrue(passwd != 'password123', msg)
    AssertionError: False is not true : user tom has a weak password

    ----------------------------------------------------------------------
    Ran 2 tests in 0.001s

    FAILED (failures=1)

    也可以在命令行中一次指定多个测试用例类,具体的大家可以自己尝试一下。

    7. 各种断言方法

    2018年10月7日 星期日

    11:45

    背景

    unittest支持各种断言方法。

    断言列表

    官方文档

    方法

    检查点

    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

    assertIsNone(x)

    x is None

    assertIsNotNone(x)

    x is not None

    assertIn(a, b)

    a in b

    assertNotIn(a, b)

    a not in b

    assertIsInstance(a, b)

    isinstance(a, b)

    assertNotIsInstance(a, b)

    not isinstance(a, b)

    assertRaises(exc, fun, *args, **kwds)

    fun(*args, **kwds) raises exc

    assertRaisesRegexp(exc, r, fun, *args, **kwds)

    fun(*args, **kwds) raises exc and the message matches regex r

    assertAlmostEqual(a, b)

    round(a-b, 7) == 0

    assertNotAlmostEqual(a, b)

    round(a-b, 7) != 0

    assertGreater(a, b)

    a > b 2.7

    assertGreaterEqual(a, b)

    a >= b

    assertLess(a, b)

    a < b

    assertLessEqual(a, b)

    a <= b

    assertRegexpMatches(s, r)

    r.search(s)

    assertNotRegexpMatches(s, r)

    not r.search(s)

    assertItemsEqual(a, b)

    sorted(a) == sorted(b) 也支持unhashable对象

    assertDictContainsSubset(a, b)

    a里面所有的键值对都在b中存在

    自己动手

    大家可以自己动手亲自尝试使用这些断言方法。

    8. 断言异常

    2018年10月7日 星期日

    11:45

    背景

    我们有时候需要断言一些方法会抛出异常,这些异常需要符合我们的预期。

    代码

    新建test_exception.py文件,内容如下

    import unittest

    class DivZeroTestCase(unittest.TestCase):

    def test_should_raise_exception(self):
            with self.assertRaises(ZeroDivisionError):
                1 / 0

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

    运行及结果

    $ python test_exception.py
    .
    ----------------------------------------------------------------------
    Ran 1 test in 0.000s

    OK

    我们能学到什么

      • 上面的例子断言了当0作为除数的时候会抛出ZeroDivisionError
      • 断言异常是有套路的,使用with语句加assertRaises,assertRaises的参数中传入预期的异常(这些异常可能需要先import进来),在with的子句中放上会抛出异常的语句或表达式
  • 相关阅读:
    java 设计模式 (一)
    多Linux安装
    华北电力大学 研究生学院 笔记
    电力大学 专升本
    19年 考研究生 过程
    netcat-flume-logger
    大神写的K8S 二进制安装笔记
    非常全的Mybatis学习笔记
    Docker-compose部署gitlab
    docker-compose 配合 dockerfile使用
  • 原文地址:https://www.cnblogs.com/hellangels333/p/9749897.html
Copyright © 2020-2023  润新知