Python(十四)测试调试和异常

Posted HT . WANG

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python(十四)测试调试和异常相关的知识,希望对你有一定的参考价值。

1.测试stdout输出

写个测试来证明标准输出,会将文本打印到屏幕上面

使用unitest框架进行测试

# mymodule.py

def urlprint(protocol, host, domain):
    url = '://.'.format(protocol, host, domain)
    print(url)

from io import StringIO
from unittest import TestCase
from unittest.mock import patch
import mymodule

class TestURLPrint(TestCase): #单元测试类
    def test_url_gets_to_stdout(self):
        protocol = 'http'
        host = 'www'
        domain = 'example.com'
        expected_url = '://.\\n'.format(protocol, host, domain) #给定标准输出

        with patch('sys.stdout', new=StringIO()) as fake_out: #patch模拟一种输出方式
            mymodule.urlprint(protocol, host, domain)
            self.assertEqual(fake_out.getvalue(), expected_url) #通过断言判断二者是否一致

2.在单元测试中给对象打补丁

写的单元测试中需要给指定的对象打补丁, 用来断言它们在测试中的期望行为(比如,断言被调用时的参数个数,访问指定的属性等)

(1)将它当做装饰器使用

from unittest.mock import patch
import example

@patch('example.func')
def test1(x, mock_func):
    example.func(x)       # Uses patched example.func
    mock_func.assert_called_with(x) #断言被调用时的参数

(2)被当做一个上下文管理器

with patch('example.func') as mock_func:
    example.func(x)      # Uses patched example.func
    mock_func.assert_called_with(x) #断言被调用时的参数

(3)可以手动的使用它打补丁

p = patch('example.func')
mock_func = p.start() #限制作用域
example.func(x)
mock_func.assert_called_with(x) #断言被调用时的参数
p.stop() #限制作用域

注意:patch() 接受一个已存在对象的全路径名,将其替换为一个新的值。 原来的值会在装饰器函数或上下文管理器完成后自动恢复回来。因此patch()起到 限制测试作用域的作用

 3.在单元测试中测试异常情况

写个测试用例来准确的判断某个异常是否被抛出。

import unittest

# A simple function to illustrate
def parse_int(s):
    return int(s)

class TestConversion(unittest.TestCase):#测试类
    def test_bad_int(self): #测试用例
        self.assertRaises(ValueError, parse_int, 'N/A') #'N/A'表示错误提示

对比手动异常检测:

class TestConversion(unittest.TestCase):
    def test_bad_int(self):
        try:
            r = parse_int('N/A')
        except ValueError as e:
            self.assertEqual(type(e), ValueError)
        else:
            self.fail('ValueError not raised') #需要注意 没有任何异常抛出情况下需要设置处理措施

4.将测试输出用日志记录到文件中

将单元测试的输出写到到某个文件中去,而不是打印到标准输出

(1)将运行测试的结果打印到标准输出上

import unittest

class MyTest(unittest.TestCase):
    pass

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

(2)重定向输出到文件

import sys

def main(out=sys.stderr, verbosity=2):
    loader = unittest.TestLoader() #组装测试套件
    suite = loader.loadTestsFromModule(sys.modules[__name__]) #从测试类中扫描收集测试用例
    unittest.TextTestRunner(out,verbosity=verbosity).run(suite) #测试运行类

if __name__ == '__main__':
    with open('testing.out', 'w') as f:
        main(f)

5.忽略或期望测试失败

unittest 模块有装饰器可用来控制对指定测试方法的处理

import unittest
import os
import platform

class Tests(unittest.TestCase):
    def test_0(self):
        self.assertTrue(True)# 输出结果:ok

    @unittest.skip('skipped test') #skip() 装饰器能被用来忽略某个你不想运行的测试 输出结果:skipped 'skipped test'
    def test_1(self):
        self.fail('should have failed!')

    @unittest.skipIf(os.name=='posix', 'Not supported on Unix') #指定测试系统为posix 不支持unix系统 输出结果:skipped 'Not supported on Unix'
    def test_2(self):
        import winreg

    @unittest.skipUnless(platform.system() == 'Darwin', 'Mac specific test') #指定测试平台为mac 输出结果:ok(保证使用mac电脑测试)
    def test_3(self):
        self.assertTrue(True)

    @unittest.expectedFailure
    def test_4(self):
        self.assertEqual(2+2, 5)

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

6.捕获异常

(1)处理多个异常

不创建大量重复代码就能处理所有的可能异常

try:
    client_obj.get_url(url)
except (URLError, ValueError):
    client_obj.remove_url(url)
except SocketTimeout:
    client_obj.handle_url_timeout(url)

注意:异常会有层级关系,对于这种情况,你可能使用它们的一个基类来捕获所有的异常

同时except 语句是顺序检查的,第一个匹配的会执行 所以异常被第一个能识别的捕获

(2)捕获全部异常

想要捕获所有的异常,可以直接捕获 Exception 即可

try:
   ...
except Exception as e:
   ...
   log('Reason:', e)       # Important!

注意:这个将会捕获除了 SystemExit 、 KeyboardInterrupt 和 GeneratorExit 之外的所有异常。 如果你还想捕获这三个异常,将 Exception 改成 BaseException 即可。

(3)捕获自定义异常

自定义异常类应该总是继承自内置的 Exception 类, 或者是继承自那些本身就是从 Exception 继承而来的类

class NetworkError(Exception):
    pass

class HostnameError(NetworkError):
    pass

class TimeoutError(NetworkError):
    pass

class ProtocolError(NetworkError):
    pass



try:
    msg = s.recv()
except TimeoutError as e:
    ...
except ProtocolError as e:
    ...

7.输出警告信息

程序能生成警告信息(比如废弃特性或使用问题)

要输出一个警告消息,可使用 warning.warn() 函数。

import warnings

def func(x, y, logfile=None, debug=False):
    if logfile is not None:
         warnings.warn('logfile argument deprecated', DeprecationWarning)
    ...

warn() 的参数是一个警告消息和一个警告类

警告类有如下几种:

  • UserWarning,
  • DeprecationWarning,
  • SyntaxWarning,
  • RuntimeWarning,
  • ResourceWarning, 
  • FutureWarning.

对警告的处理取决于你如何运行解释器以及一些其他配置 

  1. -W 选项能控制警告消息的输出。
  2.  -W all 会输出所有警告消息,
  3. -W ignore 忽略掉所有警告,
  4. -W error 将警告转换成异常

8.调试程序崩溃错误 

例:

# sample.py

def func(n):
    return n + 10

func('Hello')

如果你的程序因为某个异常而崩溃,运行 python3 -i someprogram.py 可执行简单的调试。 -i 选项可让程序结束后打开一个交互式shell。 

bash % python3 -i sample.py
Traceback (most recent call last):
  File "sample.py", line 6, in <module>
    func('Hello')
  File "sample.py", line 4, in func
    return n + 10
TypeError: Can't convert 'int' object to str implicitly
>>> func(10)
20
>>>

部分编译环境下,可以在程序崩溃后打开Python的调试器 

>>> import pdb  
#启动python自带调试器
>>> pdb.pm()
> sample.py(4)func()
-> return n + 10
(Pdb) w
  sample.py(6)<module>()
-> func('Hello')
> sample.py(4)func()
-> return n + 10
(Pdb) print n
'Hello'
(Pdb) q
>>>
附:pdb调试命令
完整命令简写命令描述
argsa打印当前函数的参数
breakb设置断点
clearcl清除断点
condition设置条件断点
continuec或者cont继续运行,知道遇到断点或者脚本结束
disable禁用断点
enable启用断点
helph查看pdb帮助
ignore忽略断点
jumpj跳转到指定行数运行
listl列出脚本清单
nextn执行下条语句,遇到函数不进入其内部
pp打印变量值,也可以用print
quitq退出 pdb
returnr一直运行到函数返回
tbreak设置临时断点,断点只中断一次
steps执行下一条语句,遇到函数进入其内部
wherew查看所在的位置
!在pdb中执行语句

9.性能测试 

程序运行所花费的时间并做性能测试

(1)简单的想测试下你的程序整体花费的时间, 通常使用Unix时间函数time就行

bash % time python3 someprogram.py
real 0m13.937s
user 0m12.162s
sys  0m0.098s
bash %

需要一个程序各个细节的详细报告,可以使用 cProfile 模块

bash % python3 -m cProfile someprogram.py
         859647 function calls in 16.016 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   263169    0.080    0.000    0.080    0.000 someprogram.py:16(frange)
      513    0.001    0.000    0.002    0.000 someprogram.py:30(generate_mandel)
   262656    0.194    0.000   15.295    0.000 someprogram.py:32(<genexpr>)
        1    0.036    0.036   16.077   16.077 someprogram.py:4(<module>)
   262144   15.021    0.000   15.021    0.000 someprogram.py:4(in_mandelbrot)
        1    0.000    0.000    0.000    0.000 os.py:746(urandom)
        1    0.000    0.000    0.000    0.000 png.py:1056(_readable)
        1    0.000    0.000    0.000    0.000 png.py:1073(Reader)
        1    0.227    0.227    0.438    0.438 png.py:163(<module>)
      512    0.010    0.000    0.010    0.000 png.py:200(group)
    ...
bash %

(2)测试个别函数消耗时间  可以采用装饰器方法

# timethis.py

import time
from functools import wraps

def timethis(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        r = func(*args, **kwargs)
        end = time.perf_counter()
        print('. : '.format(func.__module__, func.__name__, end - start))
        return r
    return wrapper


>>> @timethis  #要使用这个装饰器,只需要将其放置在你要进行性能测试的函数定义前即可
... def countdown(n):
...     while n > 0:
...             n -= 1
...
>>> countdown(10000000)
__main__.countdown : 0.803001880645752
>>>

(3)要测试某个代码块运行时间,可以定义一个上下文管理器

from contextlib import contextmanager

@contextmanager
def timeblock(label):
    start = time.perf_counter()
    try:
        yield
    finally:
        end = time.perf_counter()
        print(' : '.format(label, end - start))



>>> with timeblock('counting'):
...     n = 10000000
...     while n > 0:
...             n -= 1
...
counting : 1.5551159381866455
>>>

注意:

当执行性能测试的时候,需要注意的是你获取的结果都是近似值。 time.perf_counter() 函数会在给定平台上获取最高精度的计时值。 不过,它仍然还是基于时钟时间,很多因素会影响到它的精确度,比如机器负载。

10.优化程序运行效能

(1)使用函数

# somescript.py

import sys
import csv

with open(sys.argv[1]) as f:
     for row in csv.reader(f):

         # Some kind of processing
         pass

#定义在全局范围的代码运行起来要比定义在函数中运行慢的多 因此,如果你想让程序运行更快些,只需要将脚本语句放入函数中即可

# somescript.py
import sys
import csv

def main(filename):
    with open(filename) as f:
         for row in csv.reader(f):
             # Some kind of processing
             pass

main(sys.argv[1])

(2)尽可能去掉属性访问

每一次使用点(.)操作符来访问属性的时候会带来额外的开销。 它会触发特定的方法,比如 __getattribute__() 和 __getattr__() ,这些方法会进行字典操作操作。

import math

def compute_roots(nums):
    result = []
    for n in nums:
        result.append(math.sqrt(n))
    return result

# Test
nums = range(1000000)
for n in range(100):
    r = compute_roots(nums)




from math import sqrt # 使用 from module import name 这样的导入形式,以及使用绑定的方法 用 sqrt() 代替了 math.sqrt()

def compute_roots(nums):

    result = []
    result_append = result.append
    for n in nums:
        result_append(sqrt(n))# The result.append() 方法被赋给一个局部变量 result_append ,然后在内部循环中使用它 这种方法只有在大量重复代码中(循环)有意义
    return result

(3)使用局部变量

import math

def compute_roots(nums):
    sqrt = math.sqrt #sqrt 从 math 模块被拿出并放入了一个局部变量中  加速原因是因为对于局部变量 sqrt 的查找要快于全局变量 sqrt
    result = []
    result_append = result.append
    for n in nums:
        result_append(sqrt(n))
    return result

(4)避免不必要的抽象

任何时候当你使用额外的处理层(比如装饰器、属性访问、描述器)去包装你的代码时,都会让程序运行变慢。 

(5)使用内置的容器

内置的数据类型比如字符串、元组、列表、集合和字典都是使用C来实现的,运行起来非常快。 如果你想自己实现新的数据结构(比如链接列表、平衡树等), 那么要想在性能上达到内置的速度几乎不可能,

开发者涨薪指南 48位大咖的思考法则、工作方式、逻辑体系

以上是关于Python(十四)测试调试和异常的主要内容,如果未能解决你的问题,请参考以下文章

Python(十四)测试调试和异常

打补丁异常

RK3399驱动开发 | 10 - RK3399以太网gmac调试

RK3399驱动开发 | 10 - RK3399以太网gmac调试

(十四)日志

Python异常处理机制调试测试