This article comes from the author: Sun Yanhui Share on GitChat, "read the original" to see what questions you have communicated with the author
1, Introduction
Starting with a simple login interface test, this paper adjusts and optimizes the interface call posture step by step;
Then the main points of the interface testing framework are briefly discussed;
Finally, we introduce the interface testing framework we are currently using pithy.
Readers are expected to have a general understanding of interface automation testing through this article.
2, Introduction
Why do interface automation tests?
Under the background of frequent iterations of Internet products, the time of regression test is less and less, so it is difficult to make a complete regression of all functions in each iteration.
However, because of its simple implementation, low maintenance cost and easy to improve coverage, interface automatic testing has attracted more and more attention.
Why write your own framework?
Using requests + unittest, it is easy to implement interface automation test, and the api of requests has been very user-friendly and simple.
However, after encapsulation (especially for specific interfaces in the company), coupled with the encapsulation of some common tools, the efficiency of business scripting can be further improved.
3, Environmental preparation
Ensure that python2.7 or above is installed on this machine, and then install the following libraries:
pip install flask pip install requests
Later, we will use flash to write an interface for testing and use requests to test.
4, Test interface preparation
Next, use flash to implement two http interfaces, one to log in and the other to query the details, but you can only create a demo.py file after logging in (note, do not use windows Notepad), copy the following code, and then save and close it.
Interface code
#!/usr/bin/python# coding=utf-8from flask import Flask, request, session, jsonify USERNAME = 'admin'PASSWORD = '123456'app = Flask(__name__) app.secret_key = 'pithy'@app.route('/login', methods=['GET', 'POST'])def login(): error = None if request.method == 'POST': if request.form['username'] != USERNAME: error = 'Invalid username' elif request.form['password'] != PASSWORD: error = 'Invalid password' else: session['logged_in'] = True return jsonify({'code': 200, 'msg': 'success'}) return jsonify({'code': 401, 'msg': error}), 401@app.route('/info', methods=['get'])def info(): if not session.get('logged_in'): return jsonify({'code': 401, 'msg': 'please login !!'}) return jsonify({'code': 200, 'msg': 'success', 'data': 'info'})if __name__ == '__main__': app.run(debug=True)
Finally, execute the following command:
python demo.py
The response is as follows:
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat
You can see that the service has been improved.
Interface information
Login interface
-
Request url
/login
-
Request method
post
-
Request parameters
|Parameter name | Parameter type | Parameter description |
| :--: | :--: | :-: |
| username | String | Login name |
| password | String | Login password | -
Response information
|Parameter name | Parameter type | Parameter description |
| :--: | :--: | :-: |
| code | Integer | Result code |
| msg | String | Result information |
Detail interface
-
Request url
/info
-
Request method
get
-
Request cookies
|Parameter name | Parameter type | Parameter description |
| :--: | :--: | :-: |
| session | String | session | -
Response information
|Parameter name | Parameter type | Parameter description |
| :--: | :--: | :-: |
| code | Integer | Result code |
| msg | String | Result information |
| data | String | Data information |
5, Write interface test
Test ideas
-
Using requests[ http://docs.python-requests.org/zh_CN/latest/user/quickstart.html ]The library simulates sending HTTP requests.
-
Use unittest in python standard library to write test case s.
Script implementation
#!/usr/bin/python# coding=utf-8import requestsimport unittestclass TestLogin(unittest.TestCase): @classmethod def setUpClass(cls): cls.login_url = 'http://127.0.0.1:5000/login' cls.info_url = 'http://127.0.0.1:5000/info' cls.username = 'admin' cls.password = '123456' def test_login(self): """ Test login """ data = { 'username': self.username, 'password': self.password } response = requests.post(self.login_url, data=data).json() assert response['code'] == 200 assert response['msg'] == 'success' def test_info(self): """ test info Interface """ data = { 'username': self.username, 'password': self.password } response_cookies = requests.post(self.login_url, data=data).cookies session = response_cookies.get('session') assert session info_cookies = { 'session': session } response = requests.get(self.info_url, cookies=info_cookies).json() assert response['code'] == 200 assert response['msg'] == 'success' assert response['data'] == 'info'
6, Optimize
Encapsulated interface call
After writing this test login script, you may find that the login may be used more than once in the test process of the whole project. If you write this every time, will it be too redundant?
Yes, it's really too redundant. Here's a simple encapsulation to encapsulate the call of the login interface into a method and expose the call parameters. The example script is as follows:
#!/usr/bin/python# coding=utf-8import requestsimport unittesttry: from urlparse import urljoinexcept ImportError: from urllib.parse import urljoinclass DemoApi(object): def __init__(self, base_url): self.base_url = base_url def login(self, username, password): """ Login interface :param username: user name :param password: password """ url = urljoin(self.base_url, 'login') data = { 'username': username, 'password': password } return requests.post(url, data=data).json() def get_cookies(self, username, password): """ Get login cookies """ url = urljoin(self.base_url, 'login') data = { 'username': username, 'password': password } return requests.post(url, data=data).cookies def info(self, cookies): """ Detail interface """ url = urljoin(self.base_url, 'info') return requests.get(url, cookies=cookies).json()class TestLogin(unittest.TestCase): @classmethod def setUpClass(cls): cls.base_url = 'http://127.0.0.1:5000' cls.username = 'admin' cls.password = '123456' cls.app = DemoApi(cls.base_url) def test_login(self): """ Test login """ response = self.app.login(self.username, self.password) assert response['code'] == 200 assert response['msg'] == 'success' def test_info(self): """ Test for details """ cookies = self.app.get_cookies(self.username, self.password) response = self.app.info(cookies) assert response['code'] == 200 assert response['msg'] == 'success' assert response['data'] == 'info'
OK, in this version, we not only encapsulate the call of the login interface into an instance method to realize reuse, but also extract the host (self.base_url).
But the problem comes again. After logging in, the http response of the login interface will session set to the client in the form of a cookie, and subsequent interfaces will use this session to request.
In addition, it is hoped that the log can be printed out during interface call for debugging or viewing in case of error.
Well, let's change it again.
Keep Cookies & add log information
The above problem can be solved by using the same Session object in the requests Library (it will also keep cookie s between all requests issued by the same Session instance). The example code is as follows:
#!/usr/bin/python# coding=utf-8import unittestfrom pprint import pprintfrom requests.sessions import Sessiontry: from urlparse import urljoinexcept ImportError: from urllib.parse import urljoinclass DemoApi(object): def __init__(self, base_url): self.base_url = base_url # Create session instance self.session = Session() def login(self, username, password): """ Login interface :param username: user name :param password: password """ url = urljoin(self.base_url, 'login') data = { 'username': username, 'password': password } response = self.session.post(url, data=data).json() print('\n*****************************************') print(u'\n1,request url: \n%s' % url) print(u'\n2,Request header information:') pprint(self.session.headers) print(u'\n3,Request parameters:') pprint(data) print(u'\n4,response:') pprint(response) return response def info(self): """ Detail interface """ url = urljoin(self.base_url, 'info') response = self.session.get(url).json() print('\n*****************************************') print(u'\n1,request url: \n%s' % url) print(u'\n2,Request header information:') pprint(self.session.headers) print(u'\n3,request cookies:') pprint(dict(self.session.cookies)) print(u'\n4,response:') pprint(response) return responseclass TestLogin(unittest.TestCase): @classmethod def setUpClass(cls): cls.base_url = 'http://127.0.0.1:5000' cls.username = 'admin' cls.password = '123456' cls.app = DemoApi(cls.base_url) def test_login(self): """ Test login """ response = self.app.login(self.username, self.password) assert response['code'] == 200 assert response['msg'] == 'success' def test_info(self): """ Test for details """ self.app.login(self.username, self.password) response = self.app.info() assert response['code'] == 200 assert response['msg'] == 'success' assert response['data'] == 'info'
When it's done, we encapsulate multiple related interface calls into a class, use the same requests Session instance to keep cookies, and print out logs during the call. All our goals have been achieved.
However, if you look at the script again, you will feel uncomfortable. In each method, you have to write print 1, 2, 3... Spell the url, and many details.
But actually we What we really need to do is spell out the key parameters (url parameters, body parameters or incoming headers information). Can we just define the necessary information, and then encapsulate other common things and put them in one place for management?
Package repeat operation
Let's sort out our needs again:
-
First, I don't want to repeat the operation of splicing URLs.
-
Then, I don't want to print the log manually every time.
-
I don't want to deal with requests session.
-
Just want to define the parameters and call it directly.
Let's take a look at what the script might look like after implementation:
class DemoApi(object): def __init__(self, base_url): self.base_url = base_url @request(url='login', method='post') def login(self, username, password): """ Login interface """ data = { 'username': username, 'password': password } return {'data': data} @request(url='info', method='get') def info(self): """ Detail interface """ pass
Log of calling login interface:
****************************************************** 1,Interface description Login interface 2,request url http://127.0.0.1:5000/login 3,Request method post 4,request headers { "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", "User-Agent": "python-requests/2.7.0 CPython/2.7.10 Darwin/16.4.0" } 5,body parameter { "password": "123456", "username": "admin" } 6,Response results { "code": 200, "msg": "success" }
Here, we use the decorator function of python to encapsulate the common features into the decorator. I feel much better now. There's nothing superfluous. We can focus on the construction of key parameters. The rest is how to implement this decorator. Let's sort out our ideas first:
-
Get decorator parameters
-
Get function / method parameters
-
Merge the decorator with the parameters defined by the function
-
Splice url
-
Process the requests session. If yes, use it. If no, generate a new one
-
Assemble all parameters, send http request and print log
Due to space constraints, the source code is no longer listed. Interested students can view the implemented source code.
Source code viewing address: https://github.com/yuyu1987/pithy-test/blob/master/pithy/api.py
7, Expand
We have defined the gesture requested by the http interface. What else can we do?
-
[x] Non HTTP protocol interface
-
[x] Test case writing
-
[x] Profile management
-
[x] Test data management
-
[x] Tool class writing
-
[x] Test report generation
-
[x] Continuous integration
-
[x] Wait, wait
There is still a lot to do. What to do, what not to do, or which to do first, I think we can judge according to the following points:
-
Is it conducive to improving team productivity?
-
Is it conducive to improving test quality?
-
Is there a ready-made wheel to use?
The following is a description of several main points, which will not be expanded due to space limitations.
Test report
This should be what everyone cares about most. After all, this is the output of the test work;
At present, the mainstream unit test boxes in python have report plug-ins, so it is not recommended to write them unless there are special requirements.
-
Pytest: Recommended pytest-html and allure pytest.
-
unittest: Recommended HTMLTestRunner.
Continuous integration
Continuous integration recommended Jenkins, a series of functions such as running environment, scheduled task, trigger operation and mail sending can be realized on Jenkins.
Test case writing
The following rules are recommended:
-
Atomicity: each use case remains independent and uncoupled from each other to reduce interference.
-
Specificity: a use case should focus on verifying one thing rather than doing many things. Do not repeat verification at a test point.
-
Stability: the vast majority of use cases should be very stable, that is, they will not often hang up due to factors other than the environment, because if there are many unstable use cases in a test project, the test results will not reflect the project quality well.
-
Clear classification: relevant use cases should be written into a module or a test class, which not only facilitates maintenance, but also improves the readability of the report.
Test tool class
This can be done according to the project situation, and strive to simplify the use of some class libraries, data processing such as database access, date and time, serialization and deserialization, or encapsulate some common operations, such as randomly generating order numbers, so as to improve the efficiency of scripting.
Test data management
Common methods include writing in the code, in the configuration file (xml, yaml, json,. py, excel, etc.) and in the database. There is nothing to recommend here. It is recommended to come according to personal preferences.
8, pithy test framework introduction
pithy It means concise and powerful, and is intended to simplify automatic interface testing and improve testing efficiency.
-
Project address: https://github.com/yuyu1987/pithy-test
-
Help documentation: http://pithy-test.readthedocs.io/
The functions currently implemented are as follows:
-
One click test project generation
-
http client encapsulation
-
thrift interface encapsulation
-
Simplify profile usage
-
Optimize the use of JSON, date and other tools
Pytest is recommended for writing test cases( https://docs.pytest.org/ ),pytest It provides many testing tools and plug-ins( http://plugincompat.herokuapp.com/ ), which can meet most test requirements.
install
pip install pithy-test pip install pytest
use
One click test project generation
>>> pithy-cli init Please select project type,input api perhaps app: api Please enter project name,as pithy-api-test: pithy-api-test Start creating pithy-api-test project Start rendering... generate api/.gitignore [√] generate api/apis/__init__.py [√] generate api/apis/pithy_api.py [√] generate api/cfg.yaml [√] generate api/db/__init__.py [√] generate api/db/pithy_db.py [√] generate api/README.MD [√] generate api/requirements.txt [√] generate api/test_suites/__init__.py [√] generate api/test_suites/test_login.py [√] generate api/utils/__init__.py [√] Generated successfully,Please use the editor to open the project
Build project tree:
>>> tree pithy-api-test pithy-api-test ├── README.MD ├── apis │ ├── __init__.py │ └── pithy_api.py ├── cfg.yaml ├── db │ ├── __init__.py │ └── pithy_db.py ├── requirements.txt ├── test_suites │ ├── __init__.py │ └── test_login.py └── utils └── __init__.py 4 directories, 10 files
Call HTTP login interface example
from pithy import request@request(url='http://httpbin.org/post', method='post')def post(self, key1='value1'): """ post method """ data = { 'key1': key1 } return dict(data=data)# use response = post('test').to_json() # analysis json character,Output as dictionary response = post('test').json # analysis json character,Output as dictionary response = post('test').to_content() # Output as string response = post('test').content # Output as string response = post('test').get_cookie() # output cookie object response = post('test').cookie # output cookie object# Result value, Suppose here response = {'a': 1, 'b': { 'c': [1, 2, 3, 4]}}response = post('13111111111', '123abc').jsonprint response.b.c # Value by point number,The result is[1, 2, 3, 4]print response('$.a') # adopt object path Value,The result is 1 for i in response('$..c[@>3]'): # Through the object path value, the result is to select the element greater than 3 in the c dictionary print i
Optimize JSON and dictionary usage
# 1. Key from pixy import JSON processor dict_data = {'a': 1, 'b': {'a': [1, 2, 3, 4]}} json_data = json.dumps(dict_data) result = JSONProcessor(json_data)print result.a # Results: 1 print result.b.a # result:[1, 2, 3, 4]# 2. Keydict of operation dictionary_ data = {'a': 1, 'b': {'a': [1, 2, 3, 4]}} result = JSONProcessor(dict_data)print result.a # 1print result.b.a # [1, 2, 3, 4]# 3. object path value raw_dict = { ' key1':{ ' key2':{ ' key3': [1, 2, 3, 4, 5, 6, 7, 8] } } } jp = JSONProcessor(raw_dict)for i in jp('$..key3[@>3]'): print i# 4. Other usage dict_1 = {'a': 'a'} json_1 = '{"b": "b"}'jp = JSONProcessor(dict_1, json_1, c='c') print(jp)
More usage: http://pithy-test.readthedocs.io/
9, Summary
In this paper, we build a simple testing framework step by step on the premise of improving the efficiency of script development.
However, due to the limited level, the topics such as initialization and cleaning of test data and how to MOCK in the test are not involved. There is still a long way to go. I hope to give you an inspiration. I hope you can give me more advice on the shortcomings. Thank you very much.
The following is the supporting information. For the friends doing [software testing], it should be the most comprehensive and complete war preparation warehouse. This warehouse has also accompanied me through the most difficult journey. I hope it can also help you!
Finally, it can be in the official account: programmer Hao. ! Get a 216 page interview classic document for software test engineers for free. Share the corresponding video learning tutorials for free! Including basic knowledge, Linux essentials, Shell, Internet program principles, Mysql database, special topics of packet capturing tools, interface testing tools, advanced testing Python programming, Web automation testing, APP automation testing, and interface automation Testing, advanced continuous integration, test architecture development, test framework, performance testing, security testing, etc.
If my blog is helpful to you and you like the content of my blog, please "like", "comment" and "collect" for three times with one click! Friends who like software testing can join our testing technology exchange group: 310357728 (there are various software testing resources and technical discussions)