This post is from the author: Sun Yanhui shared a great post on GitChat, “Read the post” to see what questions people exchanged with the author

A list,

This paper starts with a simple login interface test, adjust and optimize the interface call posture step by step;

Then the key points of interface testing framework are briefly discussed.

Finally, I introduced pithy, the interface testing framework we are currently using.

The reader is expected to have a general understanding of interface automation testing through this article.

Second, the introduction

Why do interface automation tests?

Under the background of frequent iterations of Internet products, the time for regression testing is getting less and less, and it is difficult to make a complete regression for all functions in each iteration.

However, interface automation testing has been paid more and more attention because of its characteristics such as simple implementation, low maintenance cost and easy to improve coverage.

Why write your own framework?

Interface automation testing is easy to implement using Requets + UnitTest, and the Requests API is already very user-friendly and simple.

But encapsulation (especially for company-specific interfaces), coupled with the encapsulation of some common tools, can further improve the efficiency of business scripting.

Three, environmental preparation

Make sure python2.7 or higher is installed on your machine, and then install the following libraries:

pip install flask
pip install requestsCopy the code

We’ll use Flask to write an interface for testing, using Requests.

Four, test interface preparation

Create a demo. Py file (note: don’t use Windows Notepad), copy the following code into it, save it, 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}), [email protected]('/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)Copy the code

Finally, run the following command:

python demo.pyCopy the code

The response is as follows:

 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with statCopy the code

You can see the service is up.

Interface information

Login interface

  • The request url

    /login

  • Request method

    post

  • Request parameters

    | | | parameter name parameter type parameter specifies | | : – : : | — – | : – : | | username | String | login name | | “| String | | login password

  • Response information

    | | | parameter name parameter type parameter specifies | | : – : : | — – | : – : | | code | Integer | result code | | MSG | String information | | results

Details of the interface

  • The request url

    /info

  • Request method

    get

  • Request cookies

    | | | parameter name parameter type parameter specifies | | : – : : | — – | : – : | | session | String | session |

  • Response information

    | | | parameter name parameter type parameter specifies | | : – : : | — – | : – : | | code | Integer | result code | | MSG | String information | | results | data | String | | data information

Write interface tests

Test ideas

  • Use requests [http://docs.python-requests.org/zh_CN/latest/user/quickstart.html] library simulate sending HTTP requests.

  • Write test cases using the Python standard library UnitTest.

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): """ 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): 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'Copy the code

Six, optimization

Encapsulating interface call

After writing this test login script, you may find that the login may be used more than once during the entire project testing process. Wouldn’t it be too redundant to write this every time?

The following is an example of a script that encapsulates the login interface call into a method and exposes the call parameters:

#! /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): "" Url = urlJOIN (self. base_URL, 'login') data = {'username': username, 'password': password } return requests.post(url, data=data).json() def get_cookies(self, username, password): Url = urlJOIN (self. base_URL, 'login') data = {'username': username, 'password': password } return requests.post(url, data=data).cookies def info(self, cookies): 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): """ response = self.app.login(self.username, self.password) assert response['code'] == 200 assert response['msg'] == 'success' def test_info(self): """ "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'Copy the code

OK, in this version, we are not only encapsulating the login interface call as an instance method for reuse, but also extracting host (self.base_url).

However, the problem is that after login, the HTTP response of the login interface will set the session as a cookie to the client, and all subsequent interfaces will use this session to request.

Also, during the interface call, you want to be able to print the log for debugging or error viewing.

All right, let’s make another change.

Keep cookies & add log information

Using the same Session object in the Requests library (which also holds cookies between all requests made by the same Session instance) solves this problem in the following code:

#! /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. session = session () def login(self, username, password): Url = urlJOIN (self. base_URL, 'login') data = {'username': param username: 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, headers :') pprint(self.session.headers) print(u'\n3, headers :') pprint(data :') print(u'\n4, response :') pprint(response) return response def info(self): Url = urlJOIN (self.base_URL, 'info') response = self. Session. Get (url). The json () print (' \ n * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ') print (u '\ n1, request url: \n%s' % url) print(u'\n2, headers :') pprint(self.session.headers) print(u'\n3, request cookies:') Pprint (dict(self.session.cookies)) print(u'\n4, response :') pprint(response) return responsetass 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): """ response = self.app.login(self.username, self.password) assert response['code'] == 200 assert response['msg'] == 'success' def test_info(self): Self.app.login (self.username, self.password) response = self.app.info() assert response['code'] == 200 assert response['msg'] == 'success' assert response['data'] == 'info'Copy the code

We wrapped multiple related interface calls into a single class, used the same Requests Session instance to maintain cookies, and printed out logs during the call, achieving all of our goals.

Print 1, print 2, print 3… There are urls to spell out, lots of details, etc.

However, what we really need to do is to spell out the key parameters (URL parameters, body parameters, or headers information). Can we just define the necessary information and encapsulate the other common things in one place?

Encapsulation repeat operation

Let’s rearrange our requirements:

  • First of all, you don’t want to rehash urls.

  • Then, you don’t want to manually print the log every time.

  • Don’t want to deal with Requests Sessions.

  • Just want to define the parameters directly call.

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): """ data = {' username': username, 'password': password} return {'data': Data} @request(url='info', method='get') def info(self): selfCopy the code

Logs for invoking the login interface:

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 1, 2, the request url interface description login interface http://127.0.0.1:5000/login 3, the post request method Headers {"Accept": "*/*", "accept-encoding ": "gzip, deflate", "Connection": "keep-alive"," user-agent ": "Python-requests / 2.7.cpython /2.7.10 Darwin/16.4.0"} 5, body {"password": "123456", "username": {"code": 200, "MSG ": "success"}Copy the code

Here, we use Python’s decorator capabilities to implement common features wrapped in decorators. Now that I feel much better, I have nothing more to do, so I can focus on the construction of the key parameters. What remains is how to implement the decorator.

  • Gets decorator parameters

  • Gets function/method arguments

  • Merges the decorator with the parameters defined by the function

  • Joining together the url

  • Process requests Sessions, use them if available, and generate a new one if not

  • Assemble all the parameters, send the HTTP request, and print the log

Due to space constraints, the source code is no longer listed, interested students can view the implementation of the source code.

The source code to check the address: https://github.com/yuyu1987/pithy-test/blob/master/pithy/api.py

Seven, extension,

Now that we’ve defined the posture of the HTTP interface request, what else can we do?

  • [x] Non-HTTP interface

  • [x] Test case writing

  • [x] Configuration file management

  • [x] Test data management

  • [x] Utility class writing

  • [X] Test report generation

  • [x] Continuous integration

  • [x] and so on and so on

There are still a lot of things to do. What to do and what not to do, or which to do first, I think we can judge according to the following points:

  • Does it help improve team productivity?

  • Does it help improve test quality?

  • Are there any ready-made wheels that you can use?

Here are a few main points to illustrate, limited to space, not to expand.

The test report

This should be the most important thing you care about, after all, this is the output of the testing work;

Most of the current python unit test boxes have the Report plug-in, so it is not recommended to write it yourself unless you have specific requirements.

  • Pytest: Pytest-html and Allure Pytest are recommended.

  • Unittest: HTMLTestRunner is recommended.

Continuous integration

Jenkins is recommended for continuous integration, and a series of functions such as running environment, scheduled task, trigger running, and email sending can be realized on Jenkins.

Test case writing

The following rules are recommended:

  • Atomicity: Each use case remains independent and decoupled from each other to reduce interference.

  • Specificity: A use case should focus on proving one thing, not doing many things, and one test point should not be validated twice.

  • Stability: The vast majority of use cases should be very stable, that is, they should not often fail 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 quality of the project well.

  • Clear classification: Relevant use cases should be grouped into a module or test class to facilitate maintenance and improve readability of the report.

Test tool class

This can be done on a project-by-project basis to simplify the use of libraries, database access, date-time, serialization and deserialization data processing, or encapsulate common operations such as random order number generation to improve scripting efficiency.

Test data management

Common ways are to write in code, in configuration files (XML, YAML, JSON,.py, Excel, etc.), in database, etc. There is no good recommendation, the suggestion is based on personal preference, whatever is convenient.

Viii. Introduction of Pithy Test framework

Pithy means concise and powerful, intended to simplify automated interface testing and improve testing efficiency.

  • The address of the project: https://github.com/yuyu1987/pithy-test

  • Help: http://pithy-test.readthedocs.io/

The current functions are as follows:

  • One-click generation of test items

  • The HTTP client encapsulation

  • Thrift Interface encapsulation

  • Simplify configuration file usage

  • Optimize the use of JSON, date and other tools

Write a test case is recommended to use pytest (https://docs.pytest.org/), pytest provides many test tools and plug-ins (http://plugincompat.herokuapp.com/), which can meet the demand of most of the test.

The installation

pip install pithy-test
pip install pytestCopy the code

use

One-click generation of test items

>>> Pithy-cli init Please select the project type, enter API or app: API please enter the project name, such as pithy-api-test: pithy-api-test Start create Pithy-api-test project start render... Py [√] Generate API /apis/__init__.py [√] Generate API /apis/pithy_api.py [√] Generate API /cfg.yaml [√] Generate API /db/__init__.py Py [√] Build API /db/pithy_db.py [√] Build API/readme. MD [√] Build API /requirements. TXT [√] Build API /test_suites/__init__ Py [√] Generate API /utils/__init__. Py [√] Generate API /utils/__init__Copy the code

Generate project tree:

> > > tree pithy - API - test pithy - API - test ├ ─ ─ the README. MD ├ ─ ─ apis │ ├ ─ ─ just set py │ └ ─ ─ pithy_api. Py ├ ─ ─ CFG. Yaml ├ ─ ─ the db │ ├ ─ ─ just set py │ └ ─ ─ pithy_db. Py ├ ─ ─ requirements. TXT ├ ─ ─ test_suites │ ├ ─ ─ just set py │ └ ─ ─ test_login. Py └ ─ ─ utils └─ __init__. Py 4 directories, 10 filesCopy the code

Example of invoking HTTP login interface

from pithy import request@request(url='http://httpbin.org/post', method='post')def post(self, key1='value1'): """ post method """ data = { 'key1': Return dict(data=data)# response = post('test').to_json() # response = post('test').json # Response = POST ('test').to_content() # Response = post('test').content # Response = Post ('test').get_cookie() # response = POST ('test'). Cookie # response = {'a': 1, 'b': { 'c': [1, 2, 3, 4]}} Response = post('13111111111', '123abc').jsonprint response.b.c Print response('$. A ') # print response('$. A ') # print response('$. C [@>3]'): print I, print I, print ICopy the code

Optimized JSON and dictionary usage

Dict_data = {'a': 1, 'b': {'a': 1, 'b': {'a': [1, 2, 3, 4]}} json_data = json.dumps(dict_data) result = JSONProcessor(json_data)print result. KEYdict_data = {'a': 1, 'b': {'a': [1, 2, 3, 4]}} result = JSONProcessor(dict_data)print result.a # 1print result.b.a # [1, 2, 3, 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, and other usage dict_1 = {' a ':' a '} json_1 = '{" b ":" b "}' jp = JSONProcessor (dict_1 json_1, c = 'c') print (jp)Copy the code

More usage: http://pithy-test.readthedocs.io/

Nine,

In this article, we build a simple testing framework step by step to improve the efficiency of script development.

However, due to the limitation of the level, it does not involve the initialization of test data cleaning, how to MOCK in the test and other topics. There is still a long way to go, I hope to give you an inspiration, and I hope you can give me more advice on the shortcomings. Thank you very much.


GitChat 

Share knowledge on GitChat

Use knowledge to change the world

Focus on

“Read the transcript” to view the Chat transcript