1、為什么要寫代碼實現接口自動化
大家知道很多接口測試工具可以實現對接口的測試,如postman、jmeter、fiddler等等,而且使用方便,那么為什么還要寫代碼實現接口自動化呢?工具雖然方便,但也不足之處:
測試數據不可控制
接口測試本質是對數據的測試,調用接口,輸入一些數據,隨后,接口返回一些數據。驗證接口返回數據的正確性。在用工具運行測試用例之前不得不手動向數據庫中插入測試數據。這樣我們的接口測試是不是就沒有那么“自動化了”。
無法測試加密接口
這是接口測試工具的一大硬傷,如我們前面開發的接口用工具測試完全沒有問題,但遇到需要對接口參 數進行加密/解密的接口,例如 md5、base64、aes 等常見加密方式。本書第十一章會對加密接口進行介紹。 又或者接口的參數需要使用時間戳,也是工具很難模擬的。
擴展能力不足
當我們在享受工具所帶來的便利的同時,往往也會受制于工具所帶來的局限。例如,我想將測試結果生 成 hmtl 格式測試報告,我想將測試報告發送到指定郵箱。我想對接口測試做定時任務。我想對接口測試做持續集成。這些需求都是工具難以實現的。
2、接口自動化測試設計
接口測試調用過程可以用下圖概括,增加了測試數據庫
一般的 接口工具 測試過程:
1、接口工具調用被測系統的接口(傳參 username="zhangsan")。
2、系統接口根據傳參(username="zhangsan")向 正式數據庫 中查詢數據。
3、將查詢結果組裝成一定格式的數據,并返回給被調用者。
4、人工或通過工具的斷言功能檢查接口測試的正確性。
接口自動化測試項目,為了使接口測試對數據變得可控,測試過程如下:
1、接口測試項目先向 測試數據庫 中插入測試數據(zhangsan 的個人信息)。
2、調用被測系統接口(傳參 username="zhangsan")。
3、系統接口根據傳參(username="zhangsan")向測試數據庫中進行查詢并得到 zhangsan 個人信息。
4、將查詢結果組裝成一定格式的數據,并返回給被調用者。
5、通過單元測試框架斷言接口返回的數據(zhangsan 的個人信息),并生成測試報告。
為了使正式數據庫的數據不被污染,建議使用獨立的 測試數據庫 。
2、requests庫
requests 使用的是 urllib3,因此繼承了它的所有特性。requests 支持 http 連接保持和連接池 ,支持 使用cookie保持會話 ,支持 文件上傳 ,支持 自動確定響應內容的編碼。 對request庫的更詳細的介紹可以看我之前接口測試基礎的文章:
http://www.ythuaji.com.cn/article/120675.html
http://www.ythuaji.com.cn/article/106355.html
3、接口測試代碼示例
下面以之前用 python+django 開發的用戶簽到系統為背景,展示接口測試的代碼。
為什么開發接口?開發的接口主要給誰來用?
前端和后端分離是近年來 web 應用開發的一個發展趨勢。這種模式將帶來以下優勢:
1、后端可以不用必須精通前端技術(html/javascript/css),只專注于數據的處理,對外提供 api 接口。
2、前端的專業性越來越高,通過 api 接口獲取數據,從而專注于頁面的設計。
3、前后端分離增加接口的應用范圍,開發的接口可以應用到 web 頁面上,也可以應用到移動 app 上。
在這種開發模式下,接口測試工作就會變得尤為重要了。
開發實現的接口代碼示例:
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
|
# 添加發布會接口實現 def add_event(request): eid = request.post.get( 'eid' ,'') # 發布會id name = request.post.get( 'name' ,'') # 發布會標題 limit = request.post.get( 'limit' ,'') # 限制人數 status = request.post.get( 'status' ,'') # 狀態 address = request.post.get( 'address' ,'') # 地址 start_time = request.post.get( 'start_time' ,'') # 發布會時間 if eid = = ' ' or name == ' ' or limit == ' ' or address == ' ' or start_time == ' ': return jsonresponse({ 'status' : 10021 , 'message' : 'parameter error' }) result = event.objects. filter ( id = eid) if result: return jsonresponse({ 'status' : 10022 , 'message' : 'event id already exists' }) result = event.objects. filter (name = name) if result: return jsonresponse({ 'status' : 10023 , 'message' : 'event name already exists' }) if status = = '': status = 1 try : event.objects.create( id = eid,name = name,limit = limit,address = address,status = int (status),start_time = start_time) except validationerror: error = 'start_time format error. it must be in yyyy-mm-dd hh:mm:ss format.' return jsonresponse({ 'status' : 10024 , 'message' :error}) return jsonresponse({ 'status' : 200 , 'message' : 'add event success' }) |
通過post請求接收發布會參數:發布會id、標題、人數、狀態、地址和時間等參數。
首先,判斷eid、name、limit、address、start_time等字段均不能為空,否則jsonresponse()返回相應的狀態碼和提示。jsonresponse()是一個非常有用的方法,它可以直接將字典轉化成json格式返回到客戶端。
接下來,判斷發布會id是否存在,以及發布會名稱(name)是否存在;如果存在將返回相應的狀態碼和 提示信息。
再接下來,判斷發布會狀態是否為空,如果為空,將狀態設置為1(true)。
最后,將數據插入到 event 表,在插入的過程中如果日期格式錯誤,將拋出 validationerror 異常,接收 該異常并返回相應的狀態和提示,否則,插入成功,返回狀態碼200和“add event success”的提示。
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
|
# 發布會查詢接口實現 def get_event_list(request): eid = request.get.get( "eid" , "") # 發布會id name = request.get.get( "name" , "") # 發布會名稱 if eid = = ' ' and name == ' ': return jsonresponse({ 'status' : 10021 , 'message' : 'parameter error' }) if eid ! = '': event = {} try : result = event.objects.get( id = eid) except objectdoesnotexist: return jsonresponse({ 'status' : 10022 , 'message' : 'query result is empty' }) else : event[ 'eid' ] = result. id event[ 'name' ] = result.name event[ 'limit' ] = result.limit event[ 'status' ] = result.status event[ 'address' ] = result.address event[ 'start_time' ] = result.start_time return jsonresponse({ 'status' : 200 , 'message' : 'success' , 'data' :event}) if name ! = '': datas = [] results = event.objects. filter (name__contains = name) if results: for r in results: event = {} event[ 'eid' ] = r. id event[ 'name' ] = r.name event[ 'limit' ] = r.limit event[ 'status' ] = r.status event[ 'address' ] = r.address event[ 'start_time' ] = r.start_time datas.append(event) return jsonresponse({ 'status' : 200 , 'message' : 'success' , 'data' :datas}) else : return jsonresponse({ 'status' : 10022 , 'message' : 'query result is empty' }) |
通過get請求接收發布會id和name 參數。兩個參數都是可選的。首先,判斷當兩個參數同時為空,接口返回狀態碼10021,參數錯誤。
如果發布會id不為空,優先通過id查詢,因為id的唯一性,所以,查詢結果只會有一條,將查詢結果 以 key:value 對的方式存放到定義的event字典中,并將數據字典作為整個返回字典中data對應的值返回。
name查詢為模糊查詢,查詢數據可能會有多條,返回的數據稍顯復雜;首先將查詢的每一條數據放到一 個字典event中,再把每一個字典再放到數組datas中,最后再將整個數組做為返回字典中data對應的值返回。
接口測試代碼示例
1
2
3
4
5
6
7
8
9
10
11
12
|
#查詢發布會接口測試代碼 import requests url = "http://127.0.0.1:8000/api/get_event_list/" r = requests.get(url, params = { 'eid' : '1' }) result = r.json() print (result) assert result[ 'status' ] = = 200 assert result[ 'message' ] = = "success" assert result[ 'data' ][ 'name' ] = = "xx 產品發布會" assert result[ 'data' ][ 'address' ] = = "北京林匹克公園水立方" assert result[ 'data' ][ 'start_time' ] = = "2016-10-15t18:00:00" |
因為“發布會查詢接口”是get類型,所以,通過requests庫的get()方法調用,第一個參數為調用接口的url地址,params設置接口的參數,參數以字典形式組織。
json()方法可以將接口返回的json格式的數據轉化為字典。
接下來就是通過 assert 語句對接字典中的數據進行斷言。分別斷言status、message 和data的相關數據等。
使用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
39
40
41
42
43
44
45
46
47
48
49
|
#發布會查詢接口測試代碼 import unittest import requests class geteventlisttest(unittest.testcase): def setup( self ): self .base_url = "http://127.0.0.1:8000/api/get_event_list/" def test_get_event_list_eid_null( self ): ''' eid 參數為空 ''' r = requests.get( self .base_url, params = { 'eid' :''}) result = r.json() self .assertequal(result[ 'status' ], 10021 ) self .assertequal(result[ 'message' ], 'parameter error' ) def test_get_event_list_eid_error( self ): ''' eid=901 查詢結果為空 ''' r = requests.get( self .base_url, params = { 'eid' : 901 }) result = r.json() self .assertequal(result[ 'status' ], 10022 ) self .assertequal(result[ 'message' ], 'query result is empty' ) def test_get_event_list_eid_success( self ): ''' 根據 eid 查詢結果成功 ''' r = requests.get( self .base_url, params = { 'eid' : 1 }) result = r.json() self .assertequal(result[ 'status' ], 200 ) self .assertequal(result[ 'message' ], 'success' ) self .assertequal(result[ 'data' ][ 'name' ],u 'mx6發布會' ) self .assertequal(result[ 'data' ][ 'address' ],u '北京國家會議中心' ) def test_get_event_list_nam_result_null( self ): ''' 關鍵字‘abc'查詢 ''' r = requests.get( self .base_url, params = { 'name' : 'abc' }) result = r.json() self .assertequal(result[ 'status' ], 10022 ) self .assertequal(result[ 'message' ], 'query result is empty' ) def test_get_event_list_name_find( self ): ''' 關鍵字‘發布會'模糊查詢 ''' r = requests.get( self .base_url, params = { 'name' : '發布會' }) result = r.json() self .assertequal(result[ 'status' ], 200 ) self .assertequal(result[ 'message' ], 'success' ) self .assertequal(result[ 'data' ][ 0 ][ 'name' ],u 'mx6發布會' ) self .assertequal(result[ 'data' ][ 0 ][ 'address' ],u '北京國家會議中心' ) 49if __name__ = = '__main__' : unittest.main() |
unittest單元測試框架可以幫助 組織和運行接口測試用例。
4、接口自動化測試框架實現
關于接口自動化測試,unittest 已經幫我們做了大部分工作,接下來只需要 集成數據庫操作 ,以及 htmltestrunner測試報告生成 擴展即可。
框架結構如下圖:
pyrequests 框架:
db_fixture/: 初始化接口測試數據。
interface/: 用于編寫接口自動化測試用例。
report/: 生成接口自動化測試報告。
db_config.ini : 數據庫配置文件。
htmltestrunner.py unittest 單元測試框架擴展,生成 html 格式的測試報告。
run_tests.py : 執行所有接口測試用例。
4.1、數據庫配置
首先,需要修改被測系統將數據庫指向測試數據庫。以 mysql數據庫為例,針對 django 項目而言,修改.../guest/settings.py 文件。可以在系統測試環境單獨創建一個測試庫。 這樣做的目的是讓接口測試的數據不會清空或污染到功能測試庫的數據。 其他框架開發的項目與django項目類似,這個工作一般由開發同學完成,我們測試同學更多關注的是測試框架的代碼。
4.2、框架代碼實現
4.2.1、首先,創 建數據庫配置文件.../db_config.ini
4.2.2、接下來, 簡單封裝數據庫操作,數據庫表數據的插入和清除 ,.../db_fixture/ mysql_db.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
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
76
77
78
79
80
81
82
83
|
import pymysql.cursors import os import configparser as cparser # ======== reading db_config.ini setting =========== base_dir = str (os.path.dirname(os.path.dirname(__file__))) base_dir = base_dir.replace( '\\', ' / ') file_path = base_dir + "/db_config.ini" cf = cparser.configparser() cf.read(file_path) host = cf.get( "mysqlconf" , "host" ) port = cf.get( "mysqlconf" , "port" ) db = cf.get( "mysqlconf" , "db_name" ) user = cf.get( "mysqlconf" , "user" ) password = cf.get( "mysqlconf" , "password" ) # ======== mysql base operating =================== class db: def __init__( self ): try : # connect to the database self .connection = pymysql.connect(host = host, port = int (port), user = user, password = password, db = db, charset = 'utf8mb4' , cursorclass = pymysql.cursors.dictcursor) except pymysql.err.operationalerror as e: print ( "mysql error %d: %s" % (e.args[ 0 ], e.args[ 1 ])) # clear table data def clear( self , table_name): # real_sql = "truncate table " + table_name + ";" real_sql = "delete from " + table_name + ";" with self .connection.cursor() as cursor: cursor.execute( "set foreign_key_checks=0;" ) cursor.execute(real_sql) self .connection.commit() # insert sql statement def insert( self , table_name, table_data): for key in table_data: table_data[key] = "'"+str(table_data[key])+"'" key = ',' .join(table_data.keys()) value = ',' .join(table_data.values()) real_sql = "insert into " + table_name + " (" + key + ") values (" + value + ")" #print(real_sql) with self .connection.cursor() as cursor: cursor.execute(real_sql) self .connection.commit() # close database def close( self ): self .connection.close() # init data def init_data( self , datas): for table, data in datas.items(): self .clear(table) for d in data: self .insert(table, d) self .close() if __name__ = = '__main__' : db = db() table_name = "sign_event" data = { 'id' : 1 , 'name' : '紅米' , '`limit`' : 2000 , 'status' : 1 , 'address' : '北京會展中心' , 'start_time' : '2016-08-20 00:25:42' } table_name2 = "sign_guest" db.clear(table_name) db.insert(table_name, data) db.close() |
首先,讀取 db_config.ini 配置文件。 創建 db 類,__init__()方法初始化,通過 pymysql.connect()連接數據庫。
因為這里只用到數據庫表的清除和插入,所以只創建 clear()和 insert()兩個方法。其中,insert()方法對數 據的插入做了簡單的格式轉化,可將字典轉化成 sql 插入語句,這樣格式轉化了方便了數據庫表數據的創建。
最后,通過 close()方法用于關閉數據庫連接。
4.2.3、接下來接下來 創建測試數據 ,.../db_fixture/ test_data.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
25
26
27
28
29
30
|
import sys sys.path.append( '../db_fixture' ) try : from mysql_db import db except importerror: from .mysql_db import db # create data datas = { 'sign_event' :[ { 'id' : 1 , 'name' : '紅米pro發布會' , '`limit`' : 2000 , 'status' : 1 , 'address' : '北京會展中心' , 'start_time' : '2017-08-20 14:00:00' }, { 'id' : 2 , 'name' : '可參加人數為0' , '`limit`' : 0 , 'status' : 1 , 'address' : '北京會展中心' , 'start_time' : '2017-08-20 14:00:00' }, { 'id' : 3 , 'name' : '當前狀態為0關閉' , '`limit`' : 2000 , 'status' : 0 , 'address' : '北京會展中心' , 'start_time' : '2017-08-20 14:00:00' }, { 'id' : 4 , 'name' : '發布會已結束' , '`limit`' : 2000 , 'status' : 1 , 'address' : '北京會展中心' , 'start_time' : '2001-08-20 14:00:00' }, { 'id' : 5 , 'name' : '小米5發布會' , '`limit`' : 2000 , 'status' : 1 , 'address' : '北京國家會議中心' , 'start_time' : '2017-08-20 14:00:00' }, ], 'sign_guest' :[ { 'id' : 2 , 'realname' : 'has sign' , 'phone' : 13511001101 , 'email' : '[email protected]' , 'sign' : 1 , 'event_id' : 1 }, ], } # inster table datas def init_data(): db().init_data(datas) if __name__ = = '__main__' : init_data() |
init_data()函數用于讀取 datas 字典中的數據,調用 db 類中的 clear()方法清除數據庫,然后,調用 insert() 方法插入表數據。
4.2.4、編寫 接口測試用例 。創建添加發布會接口測試文件.../interface/ add_event_test.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
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
|
import unittest import requests import os, sys parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert( 0 , parentdir) from db_fixture import test_data class addeventtest(unittest.testcase): ''' 添加發布會 ''' def setup( self ): self .base_url = "http://127.0.0.1:8000/api/add_event/" def teardown( self ): print ( self .result) def test_add_event_all_null( self ): ''' 所有參數為空 ''' payload = { 'eid' :' ',' ':' ',' limit ':' ',' address ':"",' start_time ':' '} r = requests.post( self .base_url, data = payload) self .result = r.json() self .assertequal( self .result[ 'status' ], 10021 ) self .assertequal( self .result[ 'message' ], 'parameter error' ) def test_add_event_eid_exist( self ): ''' id已經存在 ''' payload = { 'eid' : 1 , 'name' : '一加4發布會' , 'limit' : 2000 , 'address' : "深圳寶體" , 'start_time' : '2017' } r = requests.post( self .base_url, data = payload) self .result = r.json() self .assertequal( self .result[ 'status' ], 10022 ) self .assertequal( self .result[ 'message' ], 'event id already exists' ) def test_add_event_name_exist( self ): ''' 名稱已經存在 ''' payload = { 'eid' : 11 , 'name' : '紅米pro發布會' , 'limit' : 2000 , 'address' : "深圳寶體" , 'start_time' : '2017' } r = requests.post( self .base_url,data = payload) self .result = r.json() self .assertequal( self .result[ 'status' ], 10023 ) self .assertequal( self .result[ 'message' ], 'event name already exists' ) def test_add_event_data_type_error( self ): ''' 日期格式錯誤 ''' payload = { 'eid' : 11 , 'name' : '一加4手機發布會' , 'limit' : 2000 , 'address' : "深圳寶體" , 'start_time' : '2017' } r = requests.post( self .base_url,data = payload) self .result = r.json() self .assertequal( self .result[ 'status' ], 10024 ) self .assertin( 'start_time format error.' , self .result[ 'message' ]) def test_add_event_success( self ): ''' 添加成功 ''' payload = { 'eid' : 11 , 'name' : '一加4手機發布會' , 'limit' : 2000 , 'address' : "深圳寶體" , 'start_time' : '2017-05-10 12:00:00' } r = requests.post( self .base_url,data = payload) self .result = r.json() self .assertequal( self .result[ 'status' ], 200 ) self .assertequal( self .result[ 'message' ], 'add event success' ) if __name__ = = '__main__' : test_data.init_data() # 初始化接口測試數據 unittest.main() |
在測試接口之前,調用test_data.py文件中的init_data()方法初始化數據庫中的測試數據。
創建addeventtest測試類繼承 unittest.testcase 類,通過創建測試用例,調用相關接口,并驗證接口返回 的數據。
4.2.5、創建 run_tests.py 文件
當開發的接口達到一定數量后,就需要考慮 分文件分目錄 的來 劃分 接口測試用例,如何批量的執行不同文件目錄下的用例呢?unittest單元測試框架提供的 discover() 方法可以幫助我們做到這一點。并使用 htmltestrunner 擴展生成 html 格式的測試報告。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import time, sys sys.path.append( './interface' ) sys.path.append( './db_fixture' ) from htmltestrunner import htmltestrunner import unittest from db_fixture import test_data # 指定測試用例為當前文件夾下的 interface 目錄 test_dir = './interface' discover = unittest.defaulttestloader.discover(test_dir, pattern = '*_test.py' ) if __name__ = = "__main__" : test_data.init_data() # 初始化接口測試數據 now = time.strftime( "%y-%m-%d %h_%m_%s" ) filename = './report/' + now + '_result.html' fp = open (filename, 'wb' ) runner = htmltestrunner(stream = fp, title = 'guest manage system interface test report' , description = 'implementation example with: ' ) runner.run(discover) fp.close() |
首先,通過調用test_data.py文件中的init_data()函數來初始化接口測試數據。
使用unittest框架所提供的discover()方法,查找 interface/ 目錄下,所有匹配*_test.py 的測試文件(*星 號匹配任意字符)。
htmltestrunner 為unittest單元測試框架的擴展,利用它所提供的htmltestrunner()類來替換unittest單元測試框架的texttestrunner()類,從而生成html格式的測試報告。
遺憾的是htmltestrunner并不支持python3.x,大家可以在網上找到適用于python3.x的htmltestrunner.py文件,使用在自己的接口自動化工程中。
通過 time 的 strftime()方法獲取當前時間,并且轉化成一定的時間格式。作為測試報告的名稱。這樣做目的是是為了避免因為生成的報告的名稱重名而造成報告的覆蓋。最終,將測試報告存放于report/目錄下面。如下圖,一張完整的接口自動化測試報告。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://www.cnblogs.com/ailiailan/p/8535293.html