前言:
requests
是python
知名的http
爬蟲庫,同樣簡單易用,是python
開源項目的TOP10。
pytest
是python
的單元測試框架,簡單易用,在很多知名項目中應用。requests
是python
知名的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
的報告,還可以定位到具體未覆蓋到的行;pycharm
的coverage
也有類似功能。
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_attribute
和test_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_hooks
和dispatch_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_hooks
的response
的參數是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
復用測試參數
通過TestLookupDict
的test_getitem
和test_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_structures
和test_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_lowlevel
和test_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
提供tmpdir
的fixture
,可以進行文件讀寫操作測試
可以使用__file__來進行文件的只讀測試,__file__表示當前文件,不會產生副作用。
第8個技巧:使用IO模擬配合進行單元測試
2.6 request-api如何測試
requests
的測試需要httpbin
和pytest-httpbin
,前者會啟動一個本地服務,后者會安裝一個pytest插件,測試用例中可以得到httpbin
的fixture
,用來操作這個服務的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個技巧:
-
僅對
public
的接口進行測試 -
使用
mock
輔助單元測試 - 測試盡可能覆蓋目標函數的所有分支
-
使用
pytest.fixture
復用被測試對象,使用pytest.mark.parametriz
復用測試參數 - 可以從不同的層面對同一個對象進行單元測試
- 涉及環境變量的地方,可以使用上下文裝飾器進行模擬多種環境變量
-
使用
pytest.raises
對異常進行捕獲處理 - 使用IO模擬配合進行單元測試
- 構造模擬服務配合測試
到此這篇關于Python單元測試常見技巧的文章就介紹到這了,更多相關Python
單元測試技巧內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://developer.51cto.com/art/202109/683755.htm?utm_source=tuicool&utm_medium=referral