一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

腳本之家,腳本語言編程技術及教程分享平臺!
分類導航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服務器之家 - 腳本之家 - Python - Python單元測試的9個技巧

Python單元測試的9個技巧

2022-01-13 00:27肖恩 Python

這篇文章主要給大家分享的是Python單元測試常見的幾個技巧,文章會講解requests的一些細節實現以及pytest的使用等,感興趣的小伙伴不妨和小編一起閱讀下面文章 的具體內容吧

前言:

requestspython知名的http爬蟲庫,同樣簡單易用,是python開源項目的TOP10。

pytestpython的單元測試框架,簡單易用,在很多知名項目中應用。requestspython知名的http爬蟲庫,同樣簡單易用,是python開源項目的TOP10。關于這2個項目,之前都有過介紹,本文主要介紹requests項目如何使用pytest進行單元測試,會達到下面3個目標:

  • 熟練pytest的使用
  • 學習如何對項目進行單元測試
  • 深入requests的一些實現細節

本文分如下幾個部分:

  • requests項目單元測試狀況
  • 簡單工具類如何測試
  • request-api如何測試
  • 底層API測試

1、requests項目單元測試狀況

requests的單元測試代碼全部在 tests 目錄,使用 pytest.ini 進行配置。測試除pytest外,還需要安裝:

 

庫名 描述
httpbin 一個使用flask實現的http服務,可以客戶端定義http響應,主要用于測試http協議
pytest-httpbin pytest的插件,封裝httpbin的實現
pytest-mock pytest的插件,提供mock
pytest-cov pytest的插件,提供覆蓋率

 

上述依賴 master 版本在requirement-dev文件中定義;2.24.0版本會在pipenv中定義。

測試用例使用make命令,子命令在Makefile中定義, 使用make ci運行所有單元測試結果如下:

?
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
$ make ci
pytest tests --junitxml=report.xml
======================================================================================================= test session starts =======================================================================================================
platform linux -- Python 3.6.8, pytest-3.10.1, py-1.10.0, pluggy-0.13.1
rootdir: /home/work6/project/requests, inifile: pytest.ini
plugins: mock-2.0.0, httpbin-1.0.0, cov-2.9.0
collected 552 items                                                                                                                                                                                                               
 
tests/test_help.py ...                                                                                                                                                                                                      [  0%]
tests/test_hooks.py ...                                                                                                                                                                                                     [  1%]
tests/test_lowlevel.py ...............                                                                                                                                                                                      [  3%]
tests/test_packages.py ...                                                                                                                                                                                                  [  4%]
tests/test_requests.py .................................................................................................................................................................................................... [ 39%]
127.0.0.1 - - [10/Aug/2021 08:41:53] "GET /stream/4 HTTP/1.1" 200 756
.127.0.0.1 - - [10/Aug/2021 08:41:53] "GET /stream/4 HTTP/1.1" 500 59
----------------------------------------
Exception happened during processing of request from ('127.0.0.1', 46048)
Traceback (most recent call last):
  File "/usr/lib64/python3.6/wsgiref/handlers.py", line 138, in run
    self.finish_response()
x.........................................................................................                                                                                                                                 [ 56%]
tests/test_structures.py ....................                                                                                                                                                                               [ 59%]
tests/test_testserver.py ......s....                                                                                                                                                                                        [ 61%]
tests/test_utils.py ..s................................................................................................................................................................................................ssss [ 98%]
ssssss.....                                                                                                                                                                                                                 [100%]
 
----------------------------------------------------------------------------------- generated xml file: /home/work6/project/requests/report.xml -----------------------------------------------------------------------------------
======================================================================================= 539 passed, 12 skipped, 1 xfailed in 64.16 seconds ========================================================================================

可以看到requests在1分鐘內,總共通過了539個測試用例,效果還是不錯。使用 make coverage 查看單元測試覆蓋率:

?
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
$ make coverage
----------- coverage: platform linux, python 3.6.8-final-0 -----------
Name                          Stmts   Miss  Cover
-------------------------------------------------
requests/__init__.py             71     71     0%
requests/__version__.py          10     10     0%
requests/_internal_utils.py      16      5    69%
requests/adapters.py            222     67    70%
requests/api.py                  20     13    35%
requests/auth.py                174     54    69%
requests/certs.py                 4      4     0%
requests/compat.py               47     47     0%
requests/cookies.py             238    115    52%
requests/exceptions.py           35     29    17%
requests/help.py                 63     19    70%
requests/hooks.py                15      4    73%
requests/models.py              455    119    74%
requests/packages.py             16     16     0%
requests/sessions.py            283     67    76%
requests/status_codes.py         15     15     0%
requests/structures.py           40     19    52%
requests/utils.py               465    170    63%
-------------------------------------------------
TOTAL                          2189    844    61%
Coverage XML written to file coverage.xml

結果顯示requests項目總體覆蓋率61%,每個模塊的覆蓋率也清晰可見。

單元測試覆蓋率使用代碼行數進行判斷,Stmts顯示模塊的有效行數,Miss顯示未執行到的行。如果生成html的報告,還可以定位到具體未覆蓋到的行;pycharmcoverage也有類似功能。

tests下的文件及測試類如下表:

 

文件 描述
compat python2和python3兼容
conftest pytest配置
test_help,test_packages,test_hooks,test_structures 簡單測試類
utils.py 工具函數
test_utils 測試工具函數
test_requests 測試requests
testserver\server 模擬服務
test_testserver 模擬服務測試
test_lowlevel 使用模擬服務測試模擬網絡測試

 

2、簡單工具類如何測試

2.1 test_help 實現分析

先從最簡單的test_help上手,測試類和被測試對象命名是對應的。先看看被測試的模塊help.py。這個模塊主要是2個函數 info _implementation:

?
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
import idna
 
def _implementation():
    ...
     
def info():
    ...
    system_ssl = ssl.OPENSSL_VERSION_NUMBER
    system_ssl_info = {
        'version': '%x' % system_ssl if system_ssl is not None else ''
    }
    idna_info = {
        'version': getattr(idna, '__version__', ''),
    }
    ...
    return {
        'platform': platform_info,
        'implementation': implementation_info,
        'system_ssl': system_ssl_info,
        'using_pyopenssl': pyopenssl is not None,
        'pyOpenSSL': pyopenssl_info,
        'urllib3': urllib3_info,
        'chardet': chardet_info,
        'cryptography': cryptography_info,
        'idna': idna_info,
        'requests': {
            'version': requests_version,
        },
    }

info提供系統環境的信息, _implementation是其內部實現,以下劃線*_*開頭。再看測試類test_help:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from requests.help import info
 
def test_system_ssl():
    """Verify we're actually setting system_ssl when it should be available."""
    assert info()['system_ssl']['version'] != ''
 
class VersionedPackage(object):
    def __init__(self, version):
        self.__version__ = version
 
def test_idna_without_version_attribute(mocker):
    """Older versions of IDNA don't provide a __version__ attribute, verify
    that if we have such a package, we don't blow up.
    """
    mocker.patch('requests.help.idna', new=None)
    assert info()['idna'] == {'version': ''}
 
def test_idna_with_version_attribute(mocker):
    """Verify we're actually setting idna version when it should be available."""
    mocker.patch('requests.help.idna', new=VersionedPackage('2.6'))
    assert info()['idna'] == {'version': '2.6'}

首先從頭部的導入信息可以看到,僅僅對info函數進行測試,這個容易理解。info測試通過,自然覆蓋到_implementation這個內部函數。這里可以得到單元測試的第1個技巧:僅對public的接口進行測試

test_idna_without_version_attributetest_idna_with_version_attribute均有一個mocker參數,這是pytest-mock提供的功能,會自動注入一個mock實現。使用這個mock對idna模塊進行模擬

?
1
2
3
4
# 模擬空實現
mocker.patch('requests.help.idna', new=None)
# 模擬版本2.6
mocker.patch('requests.help.idna', new=VersionedPackage('2.6'))

可能大家會比較奇怪,這里patch模擬的是 requests.help.idna , 而我們在help中導入的是 inda 模塊。這是因為在requests.packages中對inda進行了模塊名重定向:

?
1
2
3
4
5
6
7
for package in ('urllib3', 'idna', 'chardet'):
    locals()[package] = __import__(package)
    # This traversal is apparently necessary such that the identities are
    # preserved (requests.packages.urllib3.* is urllib3.*)
    for mod in list(sys.modules):
        if mod == package or mod.startswith(package + '.'):
            sys.modules['requests.packages.' + mod] = sys.modules[mod]

使用mocker后,idna的__version__信息就可以進行控制,這樣info中的idna結果也就可以預期。那么可以得到第2個技巧:使用mock輔助單元測試

2.2 test_hooks 實現分析

我們繼續查看hooks如何進行測試:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from requests import hooks
 
def hook(value):
    return value[1:]
 
@pytest.mark.parametrize(
    'hooks_list, result', (
        (hook, 'ata'),
        ([hook, lambda x: None, hook], 'ta'),
    )
)
def test_hooks(hooks_list, result):
    assert hooks.dispatch_hook('response', {'response': hooks_list}, 'Data') == result
 
def test_default_hooks():
    assert hooks.default_hooks() == {'response': []}

hooks模塊的2個接口default_hooksdispatch_hook都進行了測試。其中default_hooks是純函數,無參數有返回值,這種函數最容易測試,僅僅檢查返回值是否符合預期即可。dispatch_hook會復雜一些,還涉及對回調函數(hook函數)的調用:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
def dispatch_hook(key, hooks, hook_data, **kwargs):
    """Dispatches a hook dictionary on a given piece of data."""
    hooks = hooks or {}
    hooks = hooks.get(key)
    if hooks:
        # 判斷鉤子函數
        if hasattr(hooks, '__call__'):
            hooks = [hooks]
        for hook in hooks:
            _hook_data = hook(hook_data, **kwargs)
            if _hook_data is not None:
                hook_data = _hook_data
    return hook_data

pytest.mark.parametrize提供了2組參數進行測試。第一組參數hook和ata很簡單,hook是一個函數,會對參數裁剪,去掉首位,ata是期望的返回值。test_hooksresponse的參數是Data,所以結果應該是ata。第二組參數中的第一個參數會復雜一些,變成了一個數組,首位還是hook函數,中間使用一個匿名函數,匿名函數沒有返回值,這樣覆蓋到 if _hook_data is not None: 的旁路分支。執行過程如下:

  • hook函數裁剪Data首位,剩余ata
  • 匿名函數不對結果修改,剩余ata
  • hook函數繼續裁剪ata首位,剩余ta

經過測試可以發現dispatch_hook的設計十分巧妙,使用pipeline模式,將所有的鉤子串起來,這是和事件機制不一樣的地方。細心的話,我們可以發現 if hooks: 并未進行旁路測試,這個不夠嚴謹,有違我們的第3個技巧:

測試盡可能覆蓋目標函數的所有分支

2.3 test_structures 實現分析

LookupDict的測試用例如下:

?
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
class TestLookupDict:
 
    @pytest.fixture(autouse=True)
    def setup(self):
        """LookupDict instance with "bad_gateway" attribute."""
        self.lookup_dict = LookupDict('test')
        self.lookup_dict.bad_gateway = 502
 
    def test_repr(self):
        assert repr(self.lookup_dict) == "<lookup 'test'>"
 
    get_item_parameters = pytest.mark.parametrize(
        'key, value', (
            ('bad_gateway', 502),
            ('not_a_key', None)
        )
    )
 
    @get_item_parameters
    def test_getitem(self, key, value):
        assert self.lookup_dict[key] == value
 
    @get_item_parameters
    def test_get(self, key, value):
        assert self.lookup_dict.get(key) == value

可以發現使用setup方法配合@pytest.fixture,給所有測試用例初始化了一個lookup_dict對象;同時pytest.mark.parametrize可以在不同的測試用例之間復用的,我們可以得到第4個技巧:

使用pytest.fixture復用被測試對象,使用pytest.mark.parametriz復用測試參數

通過TestLookupDicttest_getitemtest_get可以更直觀的了解LookupDict的get和__getitem__方法的作用:

?
1
2
3
4
5
6
7
8
class LookupDict(dict):
    ...
    def __getitem__(self, key):
        # We allow fall-through here, so values default to None
        return self.__dict__.get(key, None)
 
    def get(self, key, default=None):
        return self.__dict__.get(key, default)
  • get自定義字典,使其可以使用 get 方法獲取值
  • __getitem__自定義字典,使其可以使用 [] 符合獲取值

CaseInsensitiveDict的測試用例在test_structurestest_requests中都有測試,前者主要是基礎測試,后者偏向業務使用層面,我們可以看到這兩種差異:

?
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
39
40
41
42
43
44
45
46
47
class TestCaseInsensitiveDict:
 
# 類測試
 
def test_repr(self):
 
assert repr(self.case_insensitive_dict) == "{'Accept': 'application/json'}"
 
def test_copy(self):
 
copy = self.case_insensitive_dict.copy()
 
assert copy is not self.case_insensitive_dict
 
assert copy == self.case_insensitive_dict
 
class TestCaseInsensitiveDict:
 
# 使用方法測試
 
def test_delitem(self):
 
cid = CaseInsensitiveDict()
 
cid['Spam'] = 'someval'
 
del cid['sPam']
 
assert 'spam' not in cid
 
assert len(cid) == 0
 
def test_contains(self):
 
cid = CaseInsensitiveDict()
 
cid['Spam'] = 'someval'
 
assert 'Spam' in cid
 
assert 'spam' in cid
 
assert 'SPAM' in cid
 
assert 'sPam' in cid
 
assert 'notspam' not in cid

借鑒上面的測試方法,不難得出第5個技巧:

可以從不同的層面對同一個對象進行單元測試

后面的test_lowleveltest_requests也應用了這種技巧

2.4 utils.py

utils中構建了一個可以寫入env的生成器(由yield關鍵字提供),可以當上下文裝飾器使用:

?
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
import contextlib
 
import os
 
@contextlib.contextmanager
 
def override_environ(**kwargs):
 
save_env = dict(os.environ)
 
for key, value in kwargs.items():
 
if value is None:
 
del os.environ[key]
 
else:
 
os.environ[key] = value
 
try:
 
yield
 
finally:
 
os.environ.clear()
 
os.environ.update(save_env)

下面是使用方法示例:

?
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
# test_requests.py
 
kwargs = {
 
var: proxy
 
}
 
# 模擬控制proxy環境變量
 
with override_environ(**kwargs):
 
proxies = session.rebuild_proxies(prep, {})
 
def rebuild_proxies(self, prepared_request, proxies):
 
bypass_proxy = should_bypass_proxies(url, no_proxy=no_proxy)
 
def should_bypass_proxies(url, no_proxy):
 
...
 
get_proxy = lambda k: os.environ.get(k) or os.environ.get(k.upper())
 
...

得出第6個技巧:涉及環境變量的地方,可以使用上下文裝飾器進行模擬多種環境變量

2.5 utils測試用例

utils的測試用例較多,我們選擇部分進行分析。先看to_key_val_list函數:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 對象轉列表
 
def to_key_val_list(value):
 
if value is None:
 
return None
 
if isinstance(value, (str, bytes, bool, int)):
 
raise ValueError('cannot encode objects that are not 2-tuples')
 
if isinstance(value, Mapping):
 
value = value.items()
 
return list(value)

對應的測試用例TestToKeyValList:

?
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
class TestToKeyValList:
 
@pytest.mark.parametrize(
 
'value, expected', (
 
([('key', 'val')], [('key', 'val')]),
 
((('key', 'val'), ), [('key', 'val')]),
 
({'key': 'val'}, [('key', 'val')]),
 
(None, None)
 
))
 
def test_valid(self, value, expected):
 
assert to_key_val_list(value) == expected
 
def test_invalid(self):
 
with pytest.raises(ValueError):
 
to_key_val_list('string')

重點是test_invalid中使用pytest.raise對異常的處理:

第7個技巧:使用pytest.raises對異常進行捕獲處理

TestSuperLen介紹了幾種進行IO模擬測試的方法:

?
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
class TestSuperLen:
 
@pytest.mark.parametrize(
 
'stream, value', (
 
(StringIO.StringIO, 'Test'),
 
(BytesIO, b'Test'),
 
pytest.param(cStringIO, 'Test',
 
marks=pytest.mark.skipif('cStringIO is None')),
 
))
 
def test_io_streams(self, stream, value):
 
"""Ensures that we properly deal with different kinds of IO streams."""
 
assert super_len(stream()) == 0
 
assert super_len(stream(value)) == 4
 
def test_super_len_correctly_calculates_len_of_partially_read_file(self):
 
"""Ensure that we handle partially consumed file like objects."""
 
s = StringIO.StringIO()
 
s.write('foobarbogus')
 
assert super_len(s) == 0
 
@pytest.mark.parametrize(
 
'mode, warnings_num', (
 
('r', 1),
 
('rb', 0),
 
))
 
def test_file(self, tmpdir, mode, warnings_num, recwarn):
 
file_obj = tmpdir.join('test.txt')
 
file_obj.write('Test')
 
with file_obj.open(mode) as fd:
 
assert super_len(fd) == 4
 
assert len(recwarn) == warnings_num
 
def test_super_len_with_tell(self):
 
foo = StringIO.StringIO('12345')
 
assert super_len(foo) == 5
 
foo.read(2)
 
assert super_len(foo) == 3
 
def test_super_len_with_fileno(self):
 
with open(__file__, 'rb') as f:
 
length = super_len(f)
 
file_data = f.read()
 
assert length == len(file_data)

使用StringIO來模擬IO操作,可以配置各種IO的測試。當然也可以使用BytesIO/cStringIO, 不過單元測試用例一般不關注性能,StringIO簡單夠用。

pytest提供tmpdirfixture,可以進行文件讀寫操作測試

可以使用__file__來進行文件的只讀測試,__file__表示當前文件,不會產生副作用。

第8個技巧:使用IO模擬配合進行單元測試

2.6 request-api如何測試

requests的測試需要httpbinpytest-httpbin,前者會啟動一個本地服務,后者會安裝一個pytest插件,測試用例中可以得到httpbinfixture,用來操作這個服務的URL。

 

功能
TestRequests requests業務測試
TestCaseInsensitiveDict 大小寫不敏感的字典測試
TestMorselToCookieExpires cookie過期測試
TestMorselToCookieMaxAge cookie大小
TestTimeout 響應超時的測試
TestPreparingURLs URL預處理
... 一些零碎的測試用例

 

坦率的講:這個測試用例內容龐大,達到2500行。看起來是針對各種業務的零散case,我并沒有完全理順其組織邏輯。我選擇一些感興趣的業務進行介紹, 先看TimeOut的測試:

?
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
TARPIT = 'http://10.255.255.1'
 
class TestTimeout:
 
def test_stream_timeout(self, httpbin):
 
try:
 
requests.get(httpbin('delay/10'), timeout=2.0)
 
except requests.exceptions.Timeout as e:
 
assert 'Read timed out' in e.args[0].args[0]
 
@pytest.mark.parametrize(
 
'timeout', (
 
(0.1, None),
 
Urllib3Timeout(connect=0.1, read=None)
 
))
 
def test_connect_timeout(self, timeout):
 
try:
 
requests.get(TARPIT, timeout=timeout)
 
pytest.fail('The connect() request should time out.')
 
except ConnectTimeout as e:
 
assert isinstance(e, ConnectionError)
 
assert isinstance(e, Timeout)

test_stream_timeout利用httpbin創建了一個延遲10s響應的接口,然后請求本身設置成2s,這樣可以收到一個本地timeout的錯誤。test_connect_timeout則是訪問一個不存在的服務,捕獲連接超時的錯誤。

TestRequests都是對requests的業務進程測試,可以看到至少是2種:

?
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
39
40
41
42
43
44
45
class TestRequests:
 
def test_basic_building(self):
 
req = requests.Request()
 
req.url = 'http://kennethreitz.org/'
 
req.data = {'life': '42'}
 
pr = req.prepare()
 
assert pr.url == req.url
 
assert pr.body == 'life=42'
 
def test_path_is_not_double_encoded(self):
 
request = requests.Request('GET', "http://0.0.0.0/get/test case").prepare()
 
assert request.path_url == '/get/test%20case
 
...
 
def test_HTTP_200_OK_GET_ALTERNATIVE(self, httpbin):
 
r = requests.Request('GET', httpbin('get'))
 
s = requests.Session()
 
s.proxies = getproxies()
 
r = s.send(r.prepare())
 
assert r.status_code == 200
 
ef test_set_cookie_on_301(self, httpbin):
 
s = requests.session()
 
url = httpbin('cookies/set?foo=bar')
 
s.get(url)
 
assert s.cookies['foo'] == 'bar'
  • 對url進行校驗,只需要對request進行prepare,這種情況下,請求并未發送,少了網絡傳輸,測試用例會更迅速
  • 需要響應數據的情況,需要使用httbin構建真實的請求-響應數據

3、底層API測試

testserver構建一個簡單的基于線程的tcp服務,這個tcp服務具有__enter____exit__方法,還可以當一個上下文環境使用。

?
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class TestTestServer:
 
def test_basic(self):
 
"""messages are sent and received properly"""
 
question = b"success?"
 
answer = b"yeah, success"
 
def handler(sock):
 
text = sock.recv(1000)
 
assert text == question
 
sock.sendall(answer)
 
with Server(handler) as (host, port):
 
sock = socket.socket()
 
sock.connect((host, port))
 
sock.sendall(question)
 
text = sock.recv(1000)
 
assert text == answer
 
sock.close()
 
def test_text_response(self):
 
"""the text_response_server sends the given text"""
 
server = Server.text_response_server(
 
"HTTP/1.1 200 OK\r\n" +
 
"Content-Length: 6\r\n" +
 
"\r\nroflol"
 
)
 
with server as (host, port):
 
r = requests.get('http://{}:{}'.format(host, port))
 
assert r.status_code == 200
 
assert r.text == u'roflol'
 
assert r.headers['Content-Length'] == '6'

test_basic方法對Server進行基礎校驗,確保收發雙方可以正確的發送和接收數據。先是客戶端的sock發送question,然后服務端在handler中判斷收到的數據是question,確認后返回answer,最后客戶端再確認可以正確收到answer響應。test_text_response方法則不完整的測試了http協議。按照http協議的規范發送了http請求,Server.text_response_server會回顯請求。下面是模擬瀏覽器的錨點定位不會經過網絡傳輸的testcase:

?
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
39
40
41
42
43
44
45
def test_fragment_not_sent_with_request():
 
"""Verify that the fragment portion of a URI isn't sent to the server."""
 
def response_handler(sock):
 
req = consume_socket_content(sock, timeout=0.5)
 
sock.send(
 
b'HTTP/1.1 200 OK\r\n'
 
b'Content-Length: '+bytes(len(req))+b'\r\n'
 
b'\r\n'+req
 
)
 
close_server = threading.Event()
 
server = Server(response_handler, wait_to_close_event=close_server)
 
with server as (host, port):
 
url = 'http://{}:{}/path/to/thing/#view=edit&token=hunter2'.format(host, port)
 
r = requests.get(url)
 
raw_request = r.content
 
assert r.status_code == 200
 
headers, body = raw_request.split(b'\r\n\r\n', 1)
 
status_line, headers = headers.split(b'\r\n', 1)
 
assert status_line == b'GET /path/to/thing/ HTTP/1.1'
 
for frag in (b'view', b'edit', b'token', b'hunter2'):
 
assert frag not in headers
 
assert frag not in body
 
close_server.set()

可以看到請求的path /path/to/thing/#view=edit&token=hunter2,其中 # 后面的部分是本地錨點,不應該進行網絡傳輸。上面測試用例中,對接收到的響應進行判斷,鑒別響應頭和響應body中不包含這些關鍵字。

結合requests的兩個層面的測試,們可以得出第9個技巧:

構造模擬服務配合測試

小結:

簡單小結一下,從requests的單元測試實踐中,可以得到下面9個技巧:

  1. 僅對public的接口進行測試
  2. 使用mock輔助單元測試
  3. 測試盡可能覆蓋目標函數的所有分支
  4. 使用pytest.fixture復用被測試對象,使用pytest.mark.parametriz復用測試參數
  5. 可以從不同的層面對同一個對象進行單元測試
  6. 涉及環境變量的地方,可以使用上下文裝飾器進行模擬多種環境變量
  7. 使用pytest.raises對異常進行捕獲處理
  8. 使用IO模擬配合進行單元測試
  9. 構造模擬服務配合測試

到此這篇關于Python單元測試常見技巧的文章就介紹到這了,更多相關Python單元測試技巧內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!

原文鏈接:https://developer.51cto.com/art/202109/683755.htm?utm_source=tuicool&utm_medium=referral

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 手机看片黄色 | 国产免费一区二区 | 国产成人h视频在线播放网站 | 无限在线观看免费入口 | 精品国产乱码久久久久久人妻 | 四虎在线免费播放 | 亚洲六月丁香婷婷综合 | 成人综合婷婷国产精品久久免费 | 手机在线观看国产精选免费 | 亚洲精品成人456在线播放 | bdsm中国精品调教 | 欧美乱理伦另类视频 | 久久久久九九 | 国产美女下面流出白浆视频 | 视频在线观看一区二区三区 | 天天操精品 | 精品无码一区二区三区中文字幕 | 天堂在线中文无弹窗全文阅读 | 国产一区二区三区高清视频 | 精品国产mmd在线观看 | 男人狂躁女人下面的视频免费 | 欧美精品成人a多人在线观看 | 嫩交18xxxx| 亚洲欧美日韩成人一区在线 | 精品一区二区高清在线观看 | 亚洲系列国产精品制服丝袜第 | 久久久久久免费高清电影 | 把老师操了 | 韩剧hd| 亚洲一卡2卡4卡5卡6卡残暴在线 | 亚洲欧美国产自拍 | 国产免费一区二区三区免费视频 | 天天综合色天天综合色sb | 免费免费啪视频在线观播放 | 国产123区 | 国产在线乱子伦一区二区 | 亚洲高清视频在线 | 国产视频中文字幕 | 亚洲免费闲人蜜桃 | 美女在尿口隐私视频 | 久久精品一区二区三区资源网 |