【unittest单元测试框架】(1)认识 unittest


认识 unittest

1、认识单元测试

  不用单元测试框架能写单元测试吗?答案是肯定的。单元测试本质上就是通过一段代码去验证另外一段代码,所以不用单元测试框架也可以写单元测试。下面就通过例子演示:

  创建一个被测试文件 calculator.py:

# -*- coding:utf-8 -*-
# filename: calculator
# author: hello.yin


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

  程序非常简单,创建一个 Calculator 类,通过__init__()方法接收两个参数,并做 int 类型转换。创建 add()、sub()、mul()、div()方法分别进行加、减、乘、除运算。

  根据上面实现的功能,创建 test_calculator.py 文件。

# -*- coding:utf-8 -*-
# filename: calculator_test.py
# author: hello.yin

from calculator import Calculator


def test_add():
    c = Calculator(3, 5)
    result = c.add()
    assert result == 8


def test_sub():
    c = Calculator(3, 5)
    result = c.sub()
    assert result == -2


def test_mul():
    c = Calculator(3, 5)
    result = c.mul()
    assert result == 15


def test_div():
    c = Calculator(3, 5)
    result = c.div()
    assert result == 3/5


if __name__ == "__main__":
    test_add()
    test_sub()
    test_mul()
    test_div()

  在测试代码中,首先引入 calculator 文件中的 Calculator 类,并对测试数据进行初始化。接下来调用该类下面的方法,得到计算结果,并断言结果是否正确。

  这样的测试存在着一些问题。首先,我们需要自己定义断言失败的提示;其次,当一个测试函数运行失败后,后面的测试函数将不再执行;最后,执行结果无法统计

  当然,我们可以通过编写更多的代码来解决这些问题,但这就偏离了我们做单元测试的初衷。我们应该将重点放在测试本身,而不是其他上面。引入单元测试框架可以很好地解决这些问题。

  下面通过 unittest 单元测试框架重新编写测试用例。

# -*- coding:utf-8 -*-
# filename: calculator_test.py
# author: hello.yin
# create time:2021/11/16 11:07

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(3, 5)
        result = c.sub()
        self.assertEqual(result, -2)

    def test_mul(self):
        c = Calculator(3, 5)
        result = c.mul()
        self.assertEqual(result, 15)

    def test_div(self):
        c = Calculator(3, 5)
        result = c.div()
        self.assertEqual(result, 3/5)


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

执行结果:

C:\Users\yzp\PycharmProjects\base_practice\Scripts\python.exe D:/00test/base_practice/calculator_test.py
test_add (__main__.TestCalculator) ... ok
test_div (__main__.TestCalculator) ... ok
test_mul (__main__.TestCalculator) ... ok
test_sub (__main__.TestCalculator) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK

Process finished with exit code 0
  引入 unittest 模块。如果想用 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、Test Runner 和 Test Fixture。只有理解了这几个概念,才能理解单元测试的基本特征。

1.Test Case

Test Case 是最小的测试单元,用于检查特定输入集合的特定返回值。unittest 提供了TestCase 基类,我们创建的测试类需要继承该基类,它可以用来创建新的测试用例。

2.Test Suite

测试套件是测试用例、测试套件或两者的集合,用于组装一组要运行的测试。unittest提供了 TestSuite 类来创建测试套件。

3.Test Runner

Test Runner 是一个组件,用于协调测试的执行并向用户提供结果。Test Runner 可以使用图形界面、文本界面或返回特殊值来展示执行测试的结果。unittest 提供了 TextTestRunner类运行测试用例,为了生成 HTML 格式的测试报告,后面会选择使用 HTMLTestRunner 运行类。

4.Test Fixture

Test Fixture 代表执行一个或多个测试所需的环境准备,以及关联的清理动作。例如,创建临时或代理数据库、目录,或启动服务器进程。unittest 中提供了 setUp()/tearDown()、 setUpClass()/tearDownClass()等方法来完成这些操作。   在理解了上面几个概念之后,我们对前面的测试用例做如下修改。
# -*- coding:utf-8 -*-
# filename: calculator_test.py
# author: hello.yin
# create time:2021/11/16 11:07

import unittest
from calculator import Calculator


class TestCalculator(unittest.TestCase):

    # 测试用例执行前置动作
    def setUp(self):
        print("start test case!")

    # 测试用例执行后置动作
    def tearDown(self):
        print("end test case!")

    def test_add(self):
        c = Calculator(3, 5)
        result = c.add()
        self.assertEqual(result, 8)

    def test_sub(self):
        c = Calculator(3, 5)
        result = c.sub()
        self.assertEqual(result, -2)

    def test_mul(self):
        c = Calculator(3, 5)
        result = c.mul()
        self.assertEqual(result, 15)

    def test_div(self):
        c = Calculator(3, 5)
        result = c.div()
        self.assertEqual(result, 3/5)


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(verbosity=2)
    runner.run(suit)
  首先,创建一个测试类并继承 TestCase 类,在该类下面创建一条以“test”开头的方法为测试用例。这个前面已有说明,这里再次说明是为了强调它的重要性。   其次,在测试类中增加了 setUp()/tearDown()方法,用于定义测试用例的前置和后置动作。因为在当前测试中暂时用不上,所以这里定义了一些简单的打印。   接下来,是测试用例的执行,这里做了很大的改动。首先,抛弃了 unittest 提供的 main()方法,而是调用 TestSuite 类下面的 addTest()来添加测试用例。因为一次只能添加一条用例,所以需要指定测试类及测试方法。然后,再调用 TextTestRunner 类下面的 run()运行测试套件。   这样做确实比直接使用 main()方法要麻烦得多,但也并非没有优点。   首先,测试用例的执行顺序可以由测试套件的添加顺序控制,而 main()方法只能按照测试类、方法的名称来执行测试用例。例如,TestA 类比 TestB 类先执行,test_add()用例比test_div()用例先执行。

  其次,当一个测试文件中有很多测试用例时,并不是每次都要执行所有的测试用例,尤其是比较耗时的 UI 自动化测试。因而通过测试套件和测试运行器可以灵活地控制要执行的测试用例。

  执行结果如下。
C:\Users\yzp\PycharmProjects\base_practice\Scripts\python.exe D:/00test/base_practice/calculator_test.py
test_add (__main__.TestCalculator) ... ok
test_sub (__main__.TestCalculator) ... ok
test_mul (__main__.TestCalculator) ... ok
test_div (__main__.TestCalculator) ... ok
start test case!

end test case!
start test case!
----------------------------------------------------------------------
end test case!
start test case!
end test case!
start test case!
Ran 4 tests in 0.000s
end test case!

OK

Process finished with exit code 0

 从执行结果可以看到,setUp/tearDown 作用于每条测试用例的开始之处与结束之处。

3、断言方法

  在执行测试用例的过程中,最终测试用例执行成功与否,是通过测试得到的实际结果与预期结果进行比较得到的。unittest 框架的 TestCase 类提供的用于测试结果的断言方法如下所示:    

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

# -*- coding:utf-8 -*-
# filename: assert_test
# author: hello.yin
# create time: 2021/11/16 13:43

import unittest


class AssertTest(unittest.TestCase):

    def setUp(self):
        print("case前置执行")

    def tearDown(self):
        print("case后置执行")

    def test_assertEqual(self):
        self.assertEqual(2, 2)
        self.assertNotEqual(2, 3)

    def test_assertTrue(self):
        self.assertTrue(1)
        self.assertFalse(0)

    def test_assertIn(self):
        self.assertIn("hello", "hello.yin")
        self.assertNotIn("hello", "hi.yin")

    def test_assertIs(self):
        self.assertIs("a", "a")
        self.assertIsNone(None)


if __name__ == "__main__":

    suit = unittest.TestSuite()
    suit.addTest(AssertTest("test_assertEqual"))
    suit.addTest(AssertTest("test_assertTrue"))
    suit.addTest(AssertTest("test_assertIn"))
    suit.addTest(AssertTest("test_assertIs"))

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

执行结果:

Testing started at 13:54 ...
C:\Users\yzp\PycharmProjects\base_practice\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm 2018.2\helpers\pycharm\_jb_unittest_runner.py" --path D:/00test/base_practice/assert_test.py
Launching unittests with arguments python -m unittest D:/00test/base_practice/assert_test.py in D:\00test\base_practice
case前置执行
case后置执行
case前置执行
case后置执行
case前置执行
case后置执行
case前置执行
case后置执行


Ran 4 tests in 0.014s

OK

Process finished with exit code 0

4、测试用例的组织与 discover 方法

  前面针对 Calculator 类所编写的测试用例存在以下问题。

  首先,一个功能对应一条测试用例显然是不够的,要写多少测试用例取决于你对功能需求与测试方法的理解。   其次,是测试用例的划分,笔者建议一个测试类对应一个被测试功能。    我们可以在一个测试文件中定义多个测试类,只要它们遵循测试用例的“规则”,main()方法就可以找到并执行它们。但是,我们要测试的类或方法可能有很多。   下面开发一个功能,用于判断某年是否为闰年。创建 leap_year.py 文件。
# -*- coding:utf-8 -*-
# filename: leap_year.py
# author: hello.yin
# create time: 2021/11/16 14:13


class LeapYear:

    def __init__(self, year):
        self.year = int(year)

    def answer(self):
        year = self.year
        if year % 100 == 0:
            if year % 400 == 0:
                return "{}是闰年".format(year)
            else:
                return "{}不是闰年".format(year)
        else:
            if year % 4 == 0:
                return "{}是闰年".format(year)
            else:
                return "{}不是闰年".format(year)

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

# -*- coding:utf-8 -*-
# filename: test_leap_year.py
# author: hello.yin
# create time: 2021/11/16 14:26

from leap_year import LeapYear
import unittest


class TestLeapYear(unittest.TestCase):

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

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

    def test_2000(self):
        result = LeapYear(2000)
        self.assertEqual(result.answer(), "2000是闰年")

    def test_2100(self):
        result = LeapYear(2100)
        self.assertEqual(result.answer(), "2100不是闰年")

    def test_2004(self):
        result = LeapYear(2004)
        self.assertEqual(result.answer(), "2004是闰年")

    def test_2007(self):
        result = LeapYear(2007)
        self.assertEqual(result.answer(), "2007不是闰年")


if __name__ == "__main__":

    suit = unittest.TestSuite()
    suit.addTest(TestLeapYear("test_2000"))
    suit.addTest(TestLeapYear("test_2100"))
    suit.addTest(TestLeapYear("test_2004"))
    suit.addTest(TestLeapYear("test_2007"))

    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suit)
  显然,这里的判断闰年功能(leap_year.py)和计算器功能(calculator.py)并不相关,它们的代码分别写在两个文件当中,所以对应的测试用例最好分开,分别为test_calculator.py和 test_leap_year.py 当前目录结构如下:

  如何执行多个测试文件呢?unittest中的TestLoader类提供的discover()方法可以从多个文件中查找测试用例。   该类根据各种标准加载测试用例,并将它们返回给测试套件。正常情况下,不需要创建这个类的实例。unittest 提供了可以共享的 defaultTestLoader 类,可以使用其子类或方法创建实例,discover()方法就是其中之一。
discover(start_dir,pattern='test*.py',top_level_dir=None)
  找到指定目录及其子目录下的所有测试模块,只有匹配的文件名才能被加载。如果启动的不是顶层目录,那么顶层目录必须单独指定。
  • start_dir :待测试的模块名或测试用例目录。
  • pattern='test*.py' :测试用例文件名的匹配原则。此处匹配文件名以“test”开头的“.py”类型的文件,星号“*”表示任意多个字符。
  • top_level_dir=None:测试模块的顶层目录,如果没有顶层目录,则默认为 None。
  现在通过 discover()方法重新实现 run_tests.py 文件的功能。
# -*- coding:utf-8 -*-
# filename: run_test.py
# author: hello.yin
# create time: 2021/11/16 14:47

import unittest

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


if __name__ == "__main__":

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

 执行结果:

C:\Users\yzp\PycharmProjects\base_practice\Scripts\python.exe D:/00test/base_practice/run_test.py
start test case!
end test case!
test_add (test_calculator.TestCalculator) ... ok
start test case!
end test case!
start test case!
test_div (test_calculator.TestCalculator) ... ok
end test case!
start test case!
end test case!
case开始测试!
test_mul (test_calculator.TestCalculator) ... ok
case结束测试!
case开始测试!
case结束测试!
case开始测试!
case结束测试!
case开始测试!
case结束测试!
test_sub (test_calculator.TestCalculator) ... ok
test_2000 (test_leap_year.TestLeapYear) ... ok
test_2004 (test_leap_year.TestLeapYear) ... ok
test_2007 (test_leap_year.TestLeapYear) ... ok
test_2100 (test_leap_year.TestLeapYear) ... ok

----------------------------------------------------------------------
Ran 8 tests in 0.000s

OK

Process finished with exit code 0
  discover()方法会自动根据测试用例目录(test_dir)查找测试用例文件(test*.py),并将找到的测试用例添加到测试套件中,因此,可以直接通过 run()方法执行测试套件 suits。这种方式极大地简化了测试用例的查找,我们需要做的就是按照文件的匹配规则创建测试文件即可。