Python代码测试 - unitest\doctest\nose\pytest

本文详细介绍Pytest测试框架的使用方法,包括参数化测试、固件使用、标记等核心功能,并对比doctest、unittest及nose等其他测试框架。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

http://blog.youkuaiyun.com/pipisorry/article/details/39123651

Pytest测试框架

参数化测试

当对一个测试函数进行测试时,通常会给函数传递多组参数。比如测试账号登陆,我们需要模拟各种千奇百怪的账号密码。

当然,我们可以把这些参数写在测试函数内部进行遍历。不过虽然参数众多,但仍然是一个测试,当某组参数导致断言失败,测试也就终止了。

通过异常捕获,我们可以保证程所有参数完整执行,但要分析测试结果就需要做不少额外的工作。

在 pytest 中,我们有更好的解决方法,就是参数化测试,即每组参数都独立执行一次测试。使用的工具就是 pytest.mark.parametrize(argnames, argvalues)。

一个多参数的例子,用于校验用户密码

# test_parametrize.py

@pytest.mark.parametrize('user, passwd',
                         [('jack', 'abcdefgh'),
                          ('tom', 'a123456a')])
def test_passwd_md5(user, passwd):
    db = {
        'jack': 'e8dc4081b13434b45189a720b77b6818',
        'tom': '1702a132e769a623c1adb78353fc9503'
    }

    import hashlib

    assert hashlib.md5(passwd.encode()).hexdigest() == db[user]

固件

固件(Fixture)是一些函数,pytest 会在执行测试函数之前(或之后)加载运行它们,其中最常见的可能就是数据库的初始连接和最后关闭操作。

示例

Pytest 使用 pytest.fixture() 定义固件,下面是最简单的固件,只返回北京邮编:

# test_postcode.py

@pytest.fixture()
def postcode():
    return '010'


def test_postcode(postcode):
    assert postcode == '010'

预处理和后处理

Pytest 使用 yield 关键词将固件分为两部分,yield 之前的代码属于预处理,会在测试前执行;yield 之后的代码属于后处理,将在测试完成后执行。

以下测试模拟数据库查询,使用固件来模拟数据库的连接关闭:

# test_db.py

@pytest.fixture()
def db():
    print('Connection successful')

    yield

    print('Connection closed')

作用域

在定义固件时,通过 scope 参数声明作用域,可选项有:

function: 函数级,每个测试函数都会执行一次固件;
class: 类级别,每个测试类执行一次,所有方法都可以使用;
module: 模块级,每个模块执行一次,模块内函数和方法都可使用;
session: 会话级,一次测试只执行一次,所有被找到的函数和方法都可用。

参数化

前面有 函数的参数化测试。

因为固件也是函数,我们同样可以对固件进行参数化。

固件参数化需要使用 pytest 内置的固件 request,并通过 request.param 获取参数。

@pytest.fixture(params=[
    ('redis', '6379'),
    ('elasticsearch', '9200')
])
def param(request):
    return request.param


@pytest.fixture(autouse=True)
def db(param):
    print('\nSucceed to connect %s:%s' % param)

    yield

    print('\nSucceed to close %s:%s' % param)


def test_api():
    assert 1 == 1

标记

[pytest.mark]

自定义标记

@pytest.mark.timeout(10, "slow", method="thread")
@pytest.mark.slow
def test_function():
    ...

多个标记一起使用时,将首先迭代距离函数最近的标记。上面的示例将导致 @pytest.mark.slow 然后 @pytest.mark.timeout(...) 。

用法应该是:如pytest -m slow slow 是装饰器的名字,此命令的额意思是将运行所有使用@ pytest.mark.slow修饰器装饰的测试。[Pytest:用法]

[用属性标记测试函数][自定义标记]

[固件]

[https://learning-pytest.readthedocs.io/zh/latest/doc/test-function/parametrize.html]

官方文档[Pytest 使用手册][完整的Pytest文档]

 

-柚子皮-

 

 

doctest\unitest

一、使用doctest\unitest进行python代码测试

        对于开发者来说,最实用的帮助莫过于帮助他们编写代码文档了。pydoc模块可以根据源代码中的docstrings为任何可导入模块生成格式良好的文档。

Python包含了两个测试框架来自动测试代码以及验证代码的正确性:

1)doctest模块,该模块可以从源代码或独立文件的例子中抽取出测试用例。

2)unittest模块,该模块是一个全功能的自动化测试框架,该框架提供了对测试准备(test fixtures), 预定义测试集(predefined test suite)以及测试发现(test discovery)的支持。

 

unitest

通用测试框架[python单元测试unittest]

 

doctest

简单一点的模块,用于检查文档的,对于编写的单元测试也很在行。

doctest查找和执行交互式Python会话,检验其符合预期,常用场景如下:

  • 检验模块的文档字符串是最新的。
  • 衰退测试。
  • 书写教程。

Doctest比unittest简单,没有API,是大型测试框架的有机补充。不过doctest没有fixture,不适合复杂的场景。

[doctest — Test interactive Python examples]

[doctest – Testing through documentation]

官方文档提到的应用情景:
1.通过验证例子(doctest)检查模块的docstring是最新的。
有时候会出现代码已经改变但docstring没有更新的情况,在docstring中加入doctest可以尽量避免这种情况的发生。
2.回归测试
我的理解是当测试未通过的时候,可以把用例写在docstring里,可以方便的进行回归测试。
3.作为包或库的教程示例
一个可以执行的示例比大段的说明性文字更直观有效
 

doctest小示例

def unit_test(x):
    '''
    >>> unit_test(3)
    6
    '''
    return x*x

import doctest
if __name__ == '__main__':
    doctest.testmod()

#my_math.py

def square(x):

 

'''''

Squares a number and returns the results.

>>> square(2)

4

>>> square(3)

9

'''

return x*x


if __name__ == '__main__':

import doctest, my_math

doctest.testmod(my_math)

成功运行情况

[root@pdns0 PythonStudy]# python my_math.py -v

Trying:

square(2)

Expecting:

4

ok

Trying:

square(3)

Expecting:

9

ok

1 items had no tests:

my_math

1 items passed all tests:

2 tests in my_math.square

2 tests in 2 items.

2 passed and 0 failed.

Test passed.

把x*x改成x**x,得到错误运行情况:

[root@pdns0 PythonStudy]# python my_math.py -v

Trying:

square(2)

Expecting:

4

ok

Trying:

square(3)

Expecting:

9

**********************************************************************

File "/home/liqing/PythonStudy/my_math.py", line 8, in my_math.square

Failed example:

square(3)

Expected:

9

Got:

27

1 items had no tests:

my_math

**********************************************************************

1 items had failures:

1 of 2 in my_math.square

2 tests in 2 items.

1 passed and 1 failed.

***Test Failed*** 1 failures

 

Note:

1.">>>"与测试代码之间有个空格。
2.期望的测试结果与docstring之间要有一个空行。
    >>> add(1, 2)
    3
    a docstring   #这个会被认为是测试输出的一部分
应该写成这样
    >>> add(1, 2)
    3
 
    a docstring
3.python thefile.py -v
显示doctest详细信息
 
python2.6版本后,还可以这样执行doctest
python -m doctest -v thefile.py 

文档字符串与doctest模块

如果函数,类或者是模块的第一行是一个字符串,那么这个字符串就是一个文档字符串。可以认为包含文档字符串是一个良好的编程习惯,这是因为这些字符串可以给Python程序开发工具提供一些信息。比如,help()命令能够检测文档字符串,Python相关的IDE也能够进行检测文档字符串的工作。由于程序员倾向于在交互式shell中查看文档字符串,所以最好将这些字符串写的简短一些。例如

# mult.py
class Test:
    """
    >>> a=Test(5)
    >>> a.multiply_by_2()
    10
    """
    def __init__(self, number):
        self._number=number
 
    def multiply_by_2(self):
        return self._number*2

在编写文档时,一个常见的问题就是如何保持文档和实际代码的同步。例如,程序员也许会修改函数的实现,但是却忘记了更新文档。针对这个问题,我们可以使用doctest模块。doctest模块收集文档字符串,并对它们进行扫描,然后将它们作为测试进行执行。为了使用doctest模块,我们通常会新建一个用于测试的独立的模块。例如,如果前面的例子Test class包含在文件mult.py中,那么,你应该新建一个testmult.py文件用来测试,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# testmult.py
 
import mult, doctest
 
doctest.testmod(mult, verbose = True )
 
# Trying:
#     a=Test(5)
# Expecting nothing
# ok
# Trying:
#     a.multiply_by_2()
# Expecting:
#     10
# ok
# 3 items had no tests:
#     mult
#     mult.Test.__init__
#     mult.Test.multiply_by_2
# 1 items passed all tests:
#    2 tests in mult.Test
# 2 tests in 4 items.
# 2 passed and 0 failed.
# Test passed.

在这段代码中,doctest.testmod(module)会执行特定模块的测试,并且返回测试失败的个数以及测试的总数目。如果所有的测试都通过了,那么不会产生任何输出。否则的话,你将会看到一个失败报告,用来显示期望值和实际值之间的差别。如果你想看到测试的详细输出,你可以使用testmod(module, verbose=True).

如果不想新建一个单独的测试文件的话,那么另一种选择就是在文件末尾包含相应的测试代码:

1
2
3
if __name__ = = '__main__' :
     import doctest
     doctest.testmod()

如果想执行这类测试的话,我们可以通过-m选项调用doctest模块。通常来讲,当执行测试的时候没有任何的输出。如果想查看详细信息的话,可以加上-v选项。

$ python -m doctest -v mult.py

 

单元测试与unittest模块

如果想更加彻底地对程序进行测试,我们可以使用unittest模块。通过单元测试,开发者可以为构成程序的每一个元素(例如,独立的函数,方法,类以及模块)编写一系列独立的测试用例。当测试更大的程序时,这些测试就可以作为基石来验证程序的正确性。当我们的程序变得越来越大的时候,对不同构件的单元测试就可以组合起来成为更大的测试框架以及测试工具。这能够极大地简化软件测试的工作,为找到并解决软件问题提供了便利。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# splitter.py
import unittest
 
def split(line, types = None , delimiter = None ):
     """Splits a line of text and optionally performs type conversion.
     ...
     """
     fields = line.split(delimiter)
     if types:
         fields = [ ty(val) for ty,val in zip (types,fields) ]
     return fields
 
class TestSplitFunction(unittest.TestCase):
     def setUp( self ):
         # Perform set up actions (if any)
         pass
     def tearDown( self ):
         # Perform clean-up actions (if any)
         pass
     def testsimplestring( self ):
         r = split( 'GOOG 100 490.50' )
         self .assertEqual(r,[ 'GOOG' , '100' , '490.50' ])
     def testtypeconvert( self ):
         r = split( 'GOOG 100 490.50' ,[ str , int , float ])
         self .assertEqual(r,[ 'GOOG' , 100 , 490.5 ])
     def testdelimiter( self ):
         r = split( 'GOOG,100,490.50' ,delimiter = ',' )
         self .assertEqual(r,[ 'GOOG' , '100' , '490.50' ])
 
# Run the unittests
if __name__ = = '__main__' :
     unittest.main()
 
#...
#----------------------------------------------------------------------
#Ran 3 tests in 0.001s
 
#OK

在使用单元测试时,我们需要定义一个继承自unittest.TestCase的类。在这个类里面,每一个测试都以方法的形式进行定义,并都以test打头进行命名——例如,’testsimplestring‘,’testtypeconvert‘以及类似的命名方式(有必要强调一下,只要方法名以test打头,那么无论怎么命名都是可以的)。在每个测试中,断言可以用来对不同的条件进行检查。

测试标准输出的例子:

假如你在程序里有一个方法,这个方法的输出指向标准输出(sys.stdout)。这通常意味着是往屏幕上输出文本信息。如果你想对你的代码进行测试来证明这一点,只要给出相应的输入,那么对应的输出就会被显示出来。

1
2
3
4
5
# url.py
 
def urlprint(protocol, host, domain):
     url = '{}://{}.{}' . format (protocol, host, domain)
     print (url)

内置的print函数在默认情况下会往sys.stdout发送输出。为了测试输出已经实际到达,你可以使用一个替身对象对其进行模拟,并且对程序的期望值进行断言。unittest.mock模块中的patch()方法可以只在运行测试的上下文中才替换对象,在测试完成后就立刻返回对象原始的状态。下面是urlprint()方法的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#urltest.py
 
from io import StringIO
from unittest import TestCase
from unittest.mock import patch
import url
 
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:
             url.urlprint(protocol, host, domain)
             self .assertEqual(fake_out.getvalue(), expected_url)

urlprint()函数有三个参数,测试代码首先给每个参数赋了一个假值。变量expected_url包含了期望的输出字符串。为了能够执行测试,我们使用了unittest.mock.patch()方法作为上下文管理器,把标准输出sys.stdout替换为了StringIO对象,这样发送的标准输出的内容就会被StringIO对象所接收。变量fake_out就是在这一过程中所创建出的模拟对象,该对象能够在with所处的代码块中所使用,来进行一系列的测试检查。当with语句完成时,patch方法能够将所有的东西都复原到测试执行之前的状态,就好像测试没有执行一样,而这无需任何额外的工作。但对于某些Python的C扩展来讲,这个例子却显得毫无意义,这是因为这些C扩展程序绕过了sys.stdout的设置,直接将输出发送到了标准输出上。这个例子仅适用于纯Python代码的程序(如果你想捕获到类似C扩展的输入输出,那么你可以通过打开一个临时文件然后将标准输出重定向到该文件的技巧来进行实现)。

 

二、使用nose进行python代码测试

推荐使用nose或是py.test,它们基本上是类似的。

nose : nose is nicer testing for python.或许nose已经不是新鲜的测试框架了,现在还有很多新的测试框架诞生,不过大家都在用它,而且似乎没要离开nose的意思。

使用nose进行测试的例子

在一个以test_开头的文件中的所有以test_开头的函数,都会被调用

1
2
def test_equality():
     assert True = = False

不出所料,当运行nose的时候,我们的测试没有通过。

1
2
3
4
5
6
7
8
9
10
11
12
13
( test )jhaddad@jons-mac-pro ~VIRTUAL_ENV /src $ nosetests                                                                                                                                     
F
======================================================================
FAIL: test_nose_example.test_equality
----------------------------------------------------------------------
Traceback (most recent call last):
   File "/Users/jhaddad/.virtualenvs/test/lib/python2.7/site-packages/nose/case.py" , line 197, in runTest
     self. test (*self.arg)
   File "/Users/jhaddad/.virtualenvs/test/src/test_nose_example.py" , line 3, in test_equality
     assert True == False
AssertionError
 
----------------------------------------------------------------------

nose.tools中同样也有一些便捷的方法可以调用

1
2
3
from nose.tools import assert_true
def test_equality():
     assert_true( False )

想使用更加类似JUnit的方法

1
2
3
4
5
6
7
8
9
10
from nose.tools import assert_true
from unittest import TestCase
 
class ExampleTest(TestCase):
 
     def setUp( self ): # setUp & tearDown are both available
         self .blah = False
 
     def test_blah( self ):
         self .assertTrue( self .blah)

开始测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
( test )jhaddad@jons-mac-pro ~VIRTUAL_ENV /src $ nosetests                                                                                                                                     
F
======================================================================
FAIL: test_blah (test_nose_example.ExampleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
   File "/Users/jhaddad/.virtualenvs/test/src/test_nose_example.py" , line 11, in test_blah
     self.assertTrue(self.blah)
AssertionError: False is not true
 
----------------------------------------------------------------------
Ran 1 test in 0.003s
 
FAILED (failures=1)

卓越的Mock库包含在Python 3 中,但是如果你在使用Python 2,可以使用pypi来获取。这个测试将进行一个远程调用,但是这次调用将耗时10s。这个例子显然是人为捏造的。我们使用mock来返回样本数据而不是真正的进行调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import mock
 
from mock import patch
from time import sleep
 
class Sweetness( object ):
     def slow_remote_call( self ):
         sleep( 10 )
         return "some_data" # lets pretend we get this back from our remote api call
 
def test_long_call():
     s = Sweetness()
     result = s.slow_remote_call()
     assert result = = "some_data"

当然,我们的测试需要很长的时间。

1
2
3
4
5
( test )jhaddad@jons-mac-pro ~VIRTUAL_ENV /src $ nosetests test_mock.py                                                                                                                        
 
Ran 1 test in 10.001s
 
OK

太慢了!因此我们会问自己,我们在测试什么?我们需要测试远程调用是否有用,还是我们要测试当我们获得数据后要做什么?大多数情况下是后者。让我们摆脱这个愚蠢的远程调用吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import mock
 
from mock import patch
from time import sleep
 
class Sweetness( object ):
     def slow_remote_call( self ):
         sleep( 10 )
         return "some_data" # lets pretend we get this back from our remote api call
 
def test_long_call():
     s = Sweetness()
     with patch. object (s, "slow_remote_call" , return_value = "some_data" ):
         result = s.slow_remote_call()
     assert result = = "some_data"

好吧,让我们再试一次:

1
2
3
4
5
6
( test )jhaddad@jons-mac-pro ~VIRTUAL_ENV /src $ nosetests test_mock.py                                                                                                                        
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
 
OK

好多了。记住,这个例子进行了荒唐的简化。就我个人来讲,我仅仅会忽略从远程系统的调用,而不是我的数据库调用。

nose-progressive是一个很好的模块,它可以改善nose的输出,让错误在发生时就显示出来,而不是留到最后。如果你的测试需要花费一定的时间,那么这是件好事。
pip install nose-progressive 并且在你的nosetests中添加--with-progressive

[写给已有编程经验的 Python 初学者的总结]
from:http://blog.youkuaiyun.com/pipisorry/article/details/39123651

ref:doctest样例

Python模块——doctest

使用Python学习selenium Web应用软件测试框架

[Python 各种测试框架简介(一):doctest]

[Python 各种测试框架简介(二):unittest]

[Python 各种测试框架简介(三):nose]

[Python 各种测试框架简介(四):pytest]

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值