单元测试负责对最小的软件设计单元进行验证,它使用软件设计文档对模块的描述作为指南,对重要的程序分支进行测试以发现模块中的错误。
1.1如何使用unittest单元测试框架
1 calculator.py 2 3 # -*- coding:utf-8 -*- 4 5 class Count: 6 def __init__(self , a , b): 7 self.a = a 8 self.b = b 9 def add(self): 10 return self.a + self.b 11 12 test.py 13 14 15 import unittest 16 from calculator import Count 17 18 19 class TestCount(unittest.TestCase): 20 def setUp(self): 21 print("test start") 22 23 def tearDown(self): 24 print("test stop") 25 26 def test_add(self): 27 count = Count(3, 4) 28 self.assertEqual(count.add(), 7) 29 30 31 if __name__ == '__main__': 32 unittest.main()
引入 unittest 模块。如果想用unittest 编写测试用例,那么一定要遵守它的“规则”。
- (1)创建一个测试类,这里为TestCalculator 类,必须要继承unittest 模块的TestCase类。
- (2)创建一个测试方法,该方法必须以“test”开头。
unittest提供了全局的main()方法,main方法使用TestLoader类来搜索所有包含在该模块中以“test”命名开头的测试方法,并且自动执行他们。
一些概念:
- 1).Test Case:测试用例,就是一个完整的测试流程,包括环境的搭建,测试,环境的还原等,通过这个测试用例,可以对某个功能进行验证。
- 2).Test Suite:测试套件,用来组装多个测试用例。unittest提供了TestSuite 类来创建测试套件。
- 3).Test Runner:在unittest中,提供了TextTestRunner类运行测试用例,为了生成HTML 格式的测试报告,后面会选择使用HTMLTestRunner 运行类。
- 4).Test Fixture:对一个测试用例环境的搭建和销毁就是Test Fixture,通过覆盖测试用例的setUp和tearDown来实现。
1 2 import unittest 3 from calculator import Count 4 5 6 class Test(unittest.TestCase): 7 8 def setUp(self): 9 print("test begin") 10 def tearDown(self): 11 print("test end") 12 def test_add2(self): 13 count = Count(4, 13) 14 self.assertEqual(count.add(), 17) 15 16 def test_add(self): 17 count = Count(2, 4) 18 self.assertEqual(count.add(), 6) 19 20 if __name__ == '__main__': 21 suite = unittest.TestSuite() 22 suite.addTest(Test("test_add")) 23 runner = unittest.TextTestRunner() 24 runner.run(suite) 25 26 -------输出结果------- 27 PS E:Python> python3 test.py 28 test begin 29 test end 30 . 31 ---------------------------------------------------------------------- 32 Ran 1 test in 0.000s 33 34 OK
说明:如上首先使用unittest的TestSuite类来创建测试套件,然后通过addTest(),添加测试用例,最后通过unittest的TextTestRunner类的run方法运行测试套件。
1.2unittest提供的断言方法
unittest的TestCase类提供了许多对测试结果的判断,如下:
方法 | 检查 | 版本 | 说明 |
assertEqual(a, b) | a == b | 判断第一个参数和第二个参数是否相等,不相等则测试失败 | |
assertNotEqual(a, b) | a != b | 判断第一个参数和第二个参数是否不相等,相等则测试失败 | |
assertTrue(x) | bool(x) is True | 判读参数x是否为true,为false则测试失败 | |
assertFalse(x) | bool(x) is False | 判断参数x是否为false,为true则测试失败 | |
assertIs(a, b) | a is b | 3.1 | 判断第一个参数和第二个参数是否为同一对象,不是则测试失败 |
assertIsNot(a, b) | a is not b | 3.1 | 判断第一个参数和第二个参数是否不为同一个对象,是则测试失败 |
assertIsNone(x) | x is None | 3.1 | 判断x是否为None,不是则测试失败 |
assertIsNotNone(x) | x is not None | 3.1 | 判断x是否不为None,是则测试失败 |
assertIn(a, b) | a in b | 3.1 | 判断a是否在b中,不在则测试失败 |
assertNotIn(a, b) | a not in b | 3.1 | 判断a是否不在b中,在则测试失败 |
assertIsInstance(a, b) | isinstance(a, b) | 3.2 | 判断a是否为b的一个实例,不是则测试失败 |
assertNotIsInstance(a, b) | not isinstance(a, b) | 3.2 | 判断a是否不为b的一个实例,是则测试失败 |
1.3discover组织测试用例
当测试用例达到成百上千条时,组织测试用例时十分麻烦,unittest模块的TestLoader类提供了discover方法可以解决这样的问题。
TestLoader类根据各种标准加载测试用例,并将它们返回给测试套件,不需要创建TestLoader类的实例,unittest提供了defaultTestLoader类可以使用其子类和方法创建TestLoader实例,其中discover就是其中的一个。
1 discover(self, start_dir, pattern='test*.py', top_level_dir=None) 2 start_dir:需要测试的模块名或测试用例目录。 3 pattern:用例文件名的匹配原则。 4 top_level_dir:测试模块的顶层目录。
1 calculator.py 2 3 # -*- coding:utf-8 -*- 4 5 class Count: 6 def __init__(self, a, b): 7 self.a = a 8 self.b = b 9 10 # 加法 11 def add(self): 12 return self.a + self.b 13 14 # 减法 15 def sub(self): 16 return self.a - self.b 17 18 19 import unittest 20 from calculator import Count 21 22 testadd.py 23 24 class TestAdd(unittest.TestCase): 25 26 def setUp(self): 27 print("test start") 28 29 def tearDown(self): 30 print("test end") 31 32 def test_add(self): 33 count = Count(3, 5) 34 self.assertEqual(count.add(), 8) 35 36 def test_add2(self): 37 count = Count(5, 8) 38 self.assertEqual(count.add(), 13) 39 40 41 if __name__ == '__main__': 42 unittest.main() 43 44 testsub.py 45 46 47 import unittest 48 from calculator import Count 49 50 class TestSub(unittest.TestCase): 51 52 def setUp(self): 53 print("test start") 54 55 def tearDown(self): 56 print("test end") 57 58 def test_sub(self): 59 count = Count(7, 3) 60 self.assertEqual(count.sub(), 4) 61 62 def test_sub2(self): 63 count = Count(8, 2) 64 self.assertEqual(count.sub(), 6) 65 66 67 if __name__ == '__main__': 68 unittest.main() 69 70 runtest.py 71 72 73 import unittest 74 75 test_dir = "./" 76 discover = unittest.defaultTestLoader.discover(test_dir, pattern="test*.py") 77 78 if __name__ == '__main__': 79 runner = unittest.TextTestRunner() 80 runner.run(discover)
说明:discover会在test_dir下查找test*.py,然后将测试用例放到测试套件中,所以可以通过run方法运行。
1.4用例执行的顺序
默认情况下,unittest根据ASCII码的顺序加载测试用例:0~9,A~Z,a~z,因此TestA类会优先于TestB,TestB会优先于Testc,同样,对于测试目录和测试文件来说,也是这样来加载测试用例。
如果想要改变unittest的默认加载测试用例的顺序,可以使用unittest框架的TestSuite类的addTest方法改变测试用例的优先级。
我们第3讲中的discover方法加载测试用例的方法也是按照ASCII码的顺序加载测试用例的,所以如果我们想要提高测试用例的优先级,只能通过测试用例的命名了,如test_a优先于test_b。
执行多级目录的测试用例
当测试用例的数量达到一定量级时,就要考虑目录划分,比如规划如下测试目录。 test_project ├──/test_case/ │ ├── test_bbb/ │ │ ├── test_ccc/ │ │ │ └── test_c.py │ │ └── test_b.py │ ├── test_ddd/ │ │ └── test_d.py │ └── test_a.py └─ run_tests.py
对于上面的目录结构,如果将discover()方法中的start_dir 参数定义为“./test_case”目录,那么只能加载test_a.py 文件中的测试用例。如何让unittest 查找test_case/下子目录中的测试文件呢?方法很简单,就是在每个子目录下放一个__init__.py 文件。__init__.py 文件的作用是将一个目录标记成一个标准的Python 模块。
1.5跳过测试和预期失败
unittest框架提供了一些装饰器,使得我们可以把不想要执行的测试用例直接跳过,或者是当某一条件成立时跳过或执行测试用例,也或者把某一个用例设置为失败,不论该用例是否执行成功。
1 2 import unittest 3 4 5 class Test(unittest.TestCase): 6 7 def setUp(self): 8 print("test begin") 9 10 def tearDown(self): 11 print("test end") 12 13 @unittest.skip("不运行该测试用例,直接跳过") 14 def test_skip(self): 15 pass 16 17 @unittest.skipIf(1>2, "条件成立时,跳过该测试用例") 18 def test_if_skip(self): 19 pass 20 21 @unittest.skipUnless(1>2, "条件成立时,执行该测试用例") 22 def test_unless(self): 23 pass 24 25 @unittest.expectedFailure # 不管执行结果是否失败,都会标记为失败,但不会抛出异常信息 26 def test_expected_failure(self): 27 pass 28 29 30 if __name__ == '__main__': 31 unittest.main()
说明:上面的装饰器也可以用在类上面。
1.6组织Web测试用例
目录结构:
文件中代码如下:
1 test_baidu.py 2 3 # coding:utf-8 4 import unittest 5 from selenium import webdriver 6 import time 7 8 9 class TestBaidu(unittest.TestCase): 10 11 def setUp(self): 12 self.driver = webdriver.Firefox() 13 self.driver.maximize_window() 14 self.driver.implicitly_wait(10) 15 self.base_url = "http://www.baidu.com" 16 17 def tearDown(self): 18 self.driver.quit() 19 20 def test_baidu(self): 21 driver = self.driver 22 driver.get(self.base_url) 23 driver.find_element_by_id("kw").send_keys("selenium") 24 driver.find_element_by_id("su").click() 25 time.sleep(3) 26 self.assertEqual(driver.title, "selenium_百度搜索") 27 28 29 if __name__ == '__main__': 30 unittest.main() 31 32 test_sogou.py 33 34 # coding:utf-8 35 import unittest 36 from selenium import webdriver 37 import time 38 39 40 class TestSouGou(unittest.TestCase): 41 def setUp(self): 42 self.driver = webdriver.Firefox() 43 self.base_url = "http://www.sogou.com" 44 self.driver.maximize_window() 45 self.driver.implicitly_wait(5) 46 47 def tearDown(self): 48 self.driver.close() 49 50 def test_sogou(self): 51 driver = self.driver 52 driver.get(self.base_url) 53 driver.find_element_by_id("query").clear() 54 driver.find_element_by_id("query").send_keys("selenium") 55 driver.find_element_by_id("stb").click() 56 time.sleep(2) 57 self.assertEqual(driver.title, "selenium - 搜狗搜索") 58 59 60 if __name__ == '__main__': 61 unittest.main() 62 63 runtest.py 64 65 # coding:utf-8 66 import unittest 67 68 discover = unittest.defaultTestLoader.discover("./testpro/testcase/", "test*.py") 69 70 if __name__ == '__main__': 71 runner = unittest.TextTestRunner() 72 runner.run(discover)
在cmd中切换到testpro文件上一目录,然后执行如下命令:
1 python3 runtest.py >>./testpro/testresult/log.txt 2>&1
打开log.txt文件,查看其中内容:
1 .. 2 ---------------------------------------------------------------------- 3 Ran 2 tests in 66.344s 4 5 OK