Python测试-unittest,2022-11-27

(2022.11.27 Sun)
unittest是Python自带的单元测试框架。unittest+html和pytest+allure(测试报告)成为常用的自动测试和报告的框架组合。


unittest architecture

概念

  • test case测试用例:测试用例是测试的基本单元,用于测试一组特定输入的特定响应,unittest提供了基类unittest.TestCase用于创建测试案例。案例包括“输入用户名不输入密码,则提示密码为空”等。
  • test fixture测试脚手架:为开展测试需要进行的准备工作,以及所有相关的清理操作(cleanup actions),比如创建临时或代理数据库、目录,启动一个服务器进程等。
  • test suite测试套件:一系列的测试用例或测试套件,用于整合一些一起执行的测试。
  • test runner测试运行器:用于执行和输出测试结果的组件,可使用图形接口、文本接口,或返回运行测试结果的特定值。

案例

# unittest_basic_example01.py
import logging
import unittest

class Login:
    pass

class TestStringMethods(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        # 必须使用@classmethod 装饰器,所有test运行前运行一次
        super().setUpClass()
        logging.info("setUpClass")

    @classmethod
    def tearDownClass(cls):
        # 必须使用@classmethod, 所有test运行完后运行一次
        super().tearDownClass()
        logging.info("tearDownClass")

    def setUp(self):
        # 每个测试用例执行之后做操作
        # do preparation
        super().setUp()
        logging.info("setUp")

    def tearDown(self):
        # 每个测试用例执行之前做操作
        super().tearDown()
        logging.info("tearDown")

    def test_upper(self):
        logging.info('method test_upper is in progress')
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        logging.info('method test_isupper is in progress')
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        logging.info('method test_split is in progress')
        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__':
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO) #, datefmt="%H:%M:%S.%f"    
    unittest.main()

运行结果

$ python unittest_basic_example01.py 
2022-11-27 14:48:37,872: setUpClass
2022-11-27 14:48:37,873: setUp
2022-11-27 14:48:37,873: method test_isupper is in progress
2022-11-27 14:48:37,873: tearDown
.2022-11-27 14:48:37,873: setUp
2022-11-27 14:48:37,873: method test_split is in progress
2022-11-27 14:48:37,873: tearDown
.2022-11-27 14:48:37,873: setUp
2022-11-27 14:48:37,873: method test_upper is in progress
2022-11-27 14:48:37,873: tearDown
.2022-11-27 14:48:37,873: tearDownClass

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

OK

该案例给出若干测试用例的结果和方法的执行顺序。

案例中定义了单元测试类TestStringMethods,在import unittest之后,定义该单元测试类之时,继承unittest.TestCase,使得该类成为一个unittest类。

unittest.TestCase有四个基本方法

  • setUpClass
  • setUp
  • tearDownClass
  • tearDown

注意这四个方法针对不同的测试用例和测试用例类,两两成一对,即setUptearDownsetUpClasstearDownClass

setUpClasstearDownClass都用@classmethod装饰器装饰为类方法,这两个方法分别在TestStringMethods的所有test case之前和之后运行一次。

setUptearDown则针对test case而言,每个test case执行前、后分别执行这两个方法。用于做测试的准备和收尾工作。

(2022.12.17 Sat)这四个方法都用于对测试的准备工作,如setUpClasstearDownClass用于在测试类对象开始执行之前对类做初始化准备工作和收尾工作(如关闭什么)。setUptearDown针对test case做初始化和收尾工作。

test case都以test作为方法的开始,这个命名传统用于向test runner通知哪些方法代表着test case。

观察测试结果,两个test case中, test_isupper先于test_split执行。默认情况下,test case的执行顺序为方法名的字典序(alphabetical)。同时还有其他若干方法可以调整test case的执行顺序。

断言assert

test case中最常用的断言方法

method checks that new in
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 3.1
assertIsNot(a, b) a is not b 3.1
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b 3.1
assertNotIn(a, b) a not in b 3.1
assertIsInstance(a, b) isinstance(a, b) 3.2
assertNotIsInstance(a, b) not isinstance(a, b) 3.2
assertRaises(xxxError)

如何测试抛出异常 How to Raise an exception in unit test

(2023.02.11 Sat)
使用unittest中的assertRaises方法。考虑下面案例test_add_fish_to_aquarium.py

import unittest

def add_fish_to_aquarium(fish_list):
    if len(fish_list) > 10:
        raise ValueError("A maximum of 10 fish can be added to the aquarium")
    return {"tank_a": fish_list}

class TestAddFishToAquarium(unittest.TestCase):
    def test_add_fish_to_aquarium_success(self):
        actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
        expected = {"tank_a": ["shark", "tuna"]}
        self.assertEqual(actual, expected)

    def test_add_fish_to_aquarium_exception(self):
        too_many_fish = ["shark"] * 25
        with self.assertRaises(ValueError) as exception_context:
            add_fish_to_aquarium(fish_list=too_many_fish)
        self.assertEqual(
            str(exception_context.exception),
            "A maximum of 10 fish can be added to the aquarium"
        )

在该案例中被测试函数add_fish_to_aquarium检测输入变量长度,如果超过10则返回ValueError,和提示信息。在测试部分,使用context manager,执行被测试函数,即

with self.assertRaises(ValueError) as exception_context:
    add_fish_to_aquarium(fish_list=too_many_fish)

此时会抛出异常并保存在异常对象exception_context中。接下来判断异常对象中内容和函数内的异常信息是否一致

self.assertEqual(str(exception_context.exception), 'A maximum of 10 fish can be added to the aquarium')

至此可以实现测试代码中对异常的测试。运行在终端该文本

>> python -m unittest test_add_fish_to_aquarium.py
Output
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

TestCase的执行顺序

(2022.12.17 Sat)
unittest中各个test case的执行顺序如前面所述,按照以test_开始的test case名字的字典顺序执行。比如上一部分的案例中,test case共三个test_uppertest_isuppertest_split。按test_之后名称的字典序,则其排序为test_isuppertest_splittest_upper,而这也是运行结果中test case的排序。

除此之外,还有其他方法可以设定test case的排序

加序号

在test case的名字中加入预先设定的序号,执行时按照序号的顺序执行。

import logging
import unittest

class SeqOrder(unittest.TestCase):

  def test_3(self):
      logging.info('method step3')
      
  def test_1(self):
      logging.info('method step1')

  def test_2(self):
      logging.info('method step2')

if __name__ == '__main__':
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO) #, datefmt="%H:%M:%S.%f"    
    unittest.main()

运行结果为

 % python unittest_basic_example03.py
2022-12-17 12:07:58,319: method step1
.2022-12-17 12:07:58,319: method step2
.2022-12-17 12:07:58,319: method step3
.
----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

Monolithic test

将test case结合为一个整体,运行时按整体内部的test case排序运行。在下面案例中,test case名字不以test_作为开头,但经过self._steps方法排序(dir(self)),在执行时(test_steps),调用了self._steps生成器,依次执行test case。这种方法类似于在test case的名字中按开发者意图加入序号并按序号执行。

import logging
import unittest

class Monolithic(unittest.TestCase):

  def step3(self):
      logging.info('method step3')
      
  def step1(self):
      logging.info('method step1')

  def step2(self):
      logging.info('method step2')

  def _steps(self):
    for name in dir(self): # dir() result is implicitly sorted
      if name.startswith("step"):
        yield name, getattr(self, name) 

  def test_steps(self):
    for name, step in self._steps():
      try:
        step()
      except Exception as e:
        self.fail("{} failed ({}: {})".format(step, type(e), e))

if __name__ == '__main__':
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO) #, datefmt="%H:%M:%S.%f"    
    unittest.main()

运行结果

 % python unittest_basic_example02.py 
2022-12-17 11:34:38,555: method step1
2022-12-17 11:34:38,556: method step2
2022-12-17 11:34:38,556: method step3
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

上面代码中dir(self)返回该类的内部对象,并按序输出。

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', 
'__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', 
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 
'__weakref__', '_steps', 'step1', 'step2', 'step3', 'test_steps']

TestSuite

在test suite中加入test case,加入顺序即执行顺序。

import logging
import unittest

class TestOrder(unittest.TestCase):

  def test_1(self):
      logging.info('method step1')

  def test_2(self):
      logging.info('method step2')


class OtherOrder(unittest.TestCase):

  def test_4(self):
      logging.info('method step4')
      
  def test_3(self):
      logging.info('method step3')


def suite():
    suite = unittest.TestSuite()
    suite.addTest(OtherOrder('test_4'))
    suite.addTest(TestOrder('test_2'))
    suite.addTest(OtherOrder('test_3'))
    suite.addTest(TestOrder('test_1'))
    return suite

if __name__ == '__main__':
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO) #, datefmt="%H:%M:%S.%f"    
    runner = unittest.TextTestRunner(failfast=True)
    runner.run(suite())

运行结果如下

% python unittest_basic_example04.py
2022-12-17 12:24:12,820: method step4
.2022-12-17 12:24:12,820: method step2
.2022-12-17 12:24:12,820: method step3
.2022-12-17 12:24:12,820: method step1
.
----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK

Reference

1 经验分享:自动化测试框架之unittest,测试小涛
2 unittest教程(2w字实例合集)——Python自动化测试一文入门,是羽十八ya
3 python unittest official doc
4 Python unittest.TestCase execution order, stackoverflow

你可能感兴趣的:(Python测试-unittest,2022-11-27)