ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

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

2022-10-01 20:54:04  阅读:39  来源: 互联网

标签:


.测试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 = {}://{}.{}
.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: Cant 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
>>>
完整命令 简写命令 描述 args a 打印当前函数的参数 break b 设置断点 clear cl 清除断点 condition 无 设置条件断点 continue c或者cont 继续运行,知道遇到断点或者脚本结束 disable 无 禁用断点 enable 无 启用断点 help h 查看pdb帮助 ignore 无 忽略断点 jump j 跳转到指定行数运行 list l 列出脚本清单 next n 执行下条语句,遇到函数不进入其内部 p p 打印变量值,也可以用print quit q 退出 pdb return r 一直运行到函数返回 tbreak 无 设置临时断点,断点只中断一次 step s 执行下一条语句,遇到函数进入其内部 where w 查看所在的位置 ! 无 在pdb中执行语句
附: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来实现的,运行起来非常快。 如果你想自己实现新的数据结构(比如链接列表、平衡树等), 那么要想在性能上达到内置的速度几乎不可能,

标签:
来源:

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有