软件测试对于保证软件质量非常重要,尤其是升级过程中对代码的改动不应该影响系统的原有功能,是未来重构代码的信息保证。一般来说稍微有些规模的软件公司都有专门的测试团队来保证软件质量,但作为程序员,首先应该保证自己编写的代码准确无误地实现了预订功能。
软件测试方法有很多,从软件工程角度来讲,可以分为白盒测试和黑盒测试两大类。其中,白盒测试主要通过阅读程序源代码来判断是否符合功能要求,对于复杂的业务逻辑白盒测试难度非常大,一般以黑盒测试为主,白盒测试为辅。黑盒测试不关心模块内部实现方式,只关心其功能是否正确,通过精心设计一些测试用例来检验模块的输入和输出是否正确,最终判断其是否符合预定的功能要求。
单元测试是保证模块质量的中要手段之一,通过单元测试来管理设计好的测试用例,不仅可以避免测试过程中人工反复输入可能引入的错误,还可以重复利用设计好的测试用例,具有很好的可扩展性,大幅度缩短代码的测试时间。Python标准库unittest提供了大量用于单元测试的类和方法,其中最常用的是TestCase类,其常用方法如下表所示:
方法名称 | 功能说明 | 方法名称 | 功能说明 | |
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 | |
assertIsInstance(a,b) | isinstance(a,b) | assertNotInstance(a,b) | not instance(a,b) | |
assertIn(a,b) | a in b | assertNotIn(a,b) | a not in b | |
assertAlmostEqual(a,b) | round(a-b,7)==0 | assertNotAlmostEqual(a,b) | round(a-b,7)!=0 | |
assertGreater(a,b) | a > b | assertGreaterEqual(a,b) | a >= b | |
assertLess(a,b) | a < b | assertLessEqual(a,b) | a <= b | |
assertRegex(s,r) | r.search(s) | assertNotRegex(s,r) | not r.search(s) | |
setUp() | 每项测试开始之前自动调用该函数 | tearDown() | 每项测试完成之后自动调用该函数 |
其中,setUp()和tearDown()这两个方法比较特殊,分别在每个测试之前和之后自动调用,常用来执行数据库连接的创建与关闭、文件的打开和关闭等操作,避免编写过多的重复代码。
编写单元测试案例。
以第四章自定义栈的代码为例,演示如何利用unittest库对Stack类入栈、出栈、改变大小以及满/空测试方法进行测试,并将测试结果写入文件test_Stack_result.txt。
1 #测试的模块,Stack.py
2 import Stack
3 #单元测试标准库
4 import unittest
5
6 #自定义一个测试类
7 class TestStack(unittest.TestCase):
8
9 def setUp(self):
10 #测试之前以追加模式打卡指定文件
11 self.fp = open('test_Stack_result.txt','a+',encoding='utf-8')
12
13 def taerDown(self):
14 #测试结束后关闭文件
15 self.fp.close()
16
17 def test_isEmpty(self):
18 try:
19 s = Stack.Stack()
20 self.assertTrue(s.isEmpty())
21 self.fp.write('isEmpty passed
')
22 except Exception as e:
23 self.fp.write('isEmpty failed
')
24
25 def test_empty(self):
26 try:
27 s = Stack.Stack(5)
28 for i in ['a','b','c']:
29 s.push(i) #先往栈中放3个元素
30
31 #测试清空栈操作是否工作正常
32 s.empty()
33 self.assertTrue(s.isEmpty())
34 self.fp.write('empty passed
')
35 except Exception as e:
36 self.fp.write('empty failed')
37
38 def test_isFull(self):
39 try:
40 s = Stack.Stack(3)
41 s.push(1)
42 s.push(2)
43 s.push(3)
44 self.assertTrue(s.isFull())
45 self.fp.write('isFull passed
')
46 except Exception as e:
47 self.fp.write('isFull failed
')
48
49 def test_pushupop(self):
50 try:
51 s = Stack.Stack()
52 s.push(3)
53 #确保入栈后立刻出栈得到原来的元素
54 self.assertEqual(s.pop(),3)
55 s.push('a')
56 self.assertEqual(s.pop(),'a')
57 self.fp.write('push and pop passed
')
58 except Exception as e:
59 self.fp.write('push or pop failed
')
60
61 def test_setSize(self):
62 try:
63 s = Stack.Stack(8)
64 for i in range(8):
65 s.push(i)
66 self.assertTrue(s.isFull())
67 #测试扩大栈空间是否正常
68 s.setSize(9)
69 s.push(9)
70 self.assertTrue(s.isFull())
71 self.assertEqual(s.pop(),9)
72 #测试缩小栈空间
73 s.setSize(4)
74 self.assertTrue(s.isFull())
75 self.assertEqual(s.pop(),3)
76 self.fp.write('setSize passed
')
77 except Exception as e:
78 self.fp.write('setSize failed
')
79
80
81 if __name__ == '__main__':
82 unittest.main()
83
84 '''
85 测试结果:
86 empty passed
87 isEmpty passed
88 isFull passed
89 push and pop passed
90 '''
注意:
(1)测试用例的设计应该是完备的,应保证覆盖尽可能多的情况,尤其是要覆盖边界条件,对目标模块的功能进行充分测试,避免漏测;
(2)测试用例以及测试代码本身可能会存在Bug,通过测试并不代表目标代码没有错误,但是一般而言,不能通过测试的模块代码是存在问题的;
(3)再好的测试方法和测试用例也无法保证能够发现所有错误,只有通过改进和综合多张测试方法并且精心设计测试用例来发现尽可能多的潜在的问题;
(4)除了功能测试,还应对程序进行性能测试与安全测试,甚至还需要进行规范性测试以保证代码可读性和可维护性。
扩展知识:在工程界,不管是安全专家还是恶意攻击者,最常使用的漏洞发现和挖掘方法是Fuzz,属于“灰”盒测试技术,也可以说一种特殊的黑盒测试技术。Fuzz的主要目的是crash、bradk和destroy,Fuzz的测试用例往往是带有攻击性的畸形数据,用来触发各种类型的潜在漏洞。
拓展知识:有时候可能需要把代码执行过程中的一些调试信息、出错信息或其他信息记录下来而不影响正常的输出,这时可以使用Python标准库logging提供的功能。该模块提供的几个输出方法默认把信息输出到标准控制台 sys.stderr,可以修改这个值使得信息能够输出到文件中。
1 import sys
2 import logging
3
4 old = sys.stderr #记录下原输出目的地
5 fp = open('log_test.txt','a',encoding='utf-8') #创建日志文件
6 sys.stderr = fp #把信息输出到指定文件
7
8 logging.debug('Debugging infomation') #输出信息到文件
9 logging.info('Infomational message') #debug() 和 info()的信息一般会被忽略
10 logging.warning('Warning:config file %s not found','server.conf')
11 logging.error('Error occurred')
12 logging.critical('Critical error--shutting down')
13
14 sys.stderr = old #恢复标准控制台
15 fp.close() #关闭文件
拓展知识:软件性能测试。Python标准库 time 提供的 time() 函数来测试代码运行时间,还可以用下面的方法来测试代码的运行时间。
1 from time import time
2
3 class Timer(object):
4
5 def __enter__(self):
6 self.start = time()
7 return self
8
9 def __exit__(self,*args):
10 self.end = time()
11 self.seconds = self.end - self.start
12
13 def isPrime(n):
14 if n == 2:
15 return True
16 for i in range(2,int(n ** 0.5) + 2):
17 if n % i == 0:
18 return False
19 return True
20
21 with Timer() as t:
22 for i in range(1000):
23 isPrime(99999999999999999999999)
24
25 print(t.seconds)
26 # 0.015599966049194336
很多时候还需要检测代码运行过程中的内存占用情况,这时候可以使用pip安装Python扩展库 memory_profiler。
from memory_profiler import profile
@profile #装饰器
def isPrime(n):
if n == 2:
return True
for i in range(2,int(n ** 0.5) + 2):
if n % i == 0:
return False
return True
isPrime(99999999999999999999999)
'''
Line # Mem usage Increment Line Contents
================================================
3 16.8 MiB 16.8 MiB @profile #装饰器
4 def isPrime(n):
5 16.8 MiB 0.0 MiB if n == 2:
6 return True
7 16.8 MiB 0.0 MiB for i in range(2,int(n ** 0.5) + 2):
8 16.8 MiB 0.0 MiB if n % i == 0:
9 16.8 MiB 0.0 MiB return False
10 return True
'''