[Advanced Python] RESTful Routes for pytest
上一回说道:
$ docker-compose exec api python manage.py recreate_db # 那么自然也可以触发pytest的命令 $ docker-compose exec api python -m pytest "src/tests"
$ docker-compose exec api-db psql -U postgres
Ref: https://testdriven.io/courses/tdd-flask/restful-routes/【本篇对应教程】
Following RESTful best practices, with TDD.
基本的套路
一、注册一个 Blueprint
首先是 ./src/__init__.py 入口函数。
import os from flask import Flask from flask_sqlalchemy import SQLAlchemy # instantiate the db db = SQLAlchemy() # new def create_app(script_info=None): # instantiate the app app = Flask(__name__) # set config app_settings = os.getenv('APP_SETTINGS') app.config.from_object(app_settings) # set up extensions # 与app结合起来 db.init_app(app) ######################################## # register blueprints from src.api.ping import ping_blueprint app.register_blueprint(ping_blueprint) from src.api.users import users_blueprint app.register_blueprint(users_blueprint) # shell context for flask cli @app.shell_context_processor def ctx(): return {'app': app, 'db': db} return app
二、通过 Blueprint 定义一个API
定义好一个REST API。
# src/api/user.py
from flask import Blueprint, request from flask_restx import Resource, Api from src import db from src.api.models import User users_blueprint = Blueprint('users', __name__) api = Api(users_blueprint) class UsersList(Resource): def post(self): print("In UsersList.post()") post_data = request.get_json() username = post_data.get('username') email = post_data.get('email') db.session.add( User(username=username, email=email) ) db.session.commit()
# 测试的内容 response_object = { 'message': f'{email} was added!' } return response_object, 201 api.add_resource(UsersList, '/users')
三、测试 Blueprint
直接对 这个接口进行测试。
test_app 从哪里来?从fixture那里来。
# src/tests/test_users.py import json def test_add_user(test_app, test_database):
client = test_app.test_client() resp = client.post( '/users', data=json.dumps({ 'username': 'michael', 'email': 'michael@testdriven.io' }), content_type='application/json', )
data = json.loads(resp.data.decode())
assert resp.status_code == 201 assert 'michael@testdriven.io was added!' in data['message']
-
-
Exception --> REST API
-
What about errors and exceptions? Like:
- A payload is not sent
- The payload is invalid -- i.e., the JSON object is empty or it contains the wrong keys
- The user already exists in the database
[例如]
在上面的测试函数例子的所在文件中,可以继续添加更多的测试函数。
先 invoke rest api 一次,然后再invoke一次,就会导致 already exists。
def test_add_user_duplicate_email(test_app, test_database): client = test_app.test_client()
client.post( '/users', data=json.dumps({ 'username': 'michael', 'email': 'michael@testdriven.io' }), content_type='application/json', )
resp = client.post( '/users', data=json.dumps({ 'username': 'michael', 'email': 'michael@testdriven.io' }), content_type='application/json', )
data = json.loads(resp.data.decode()) assert resp.status_code == 400 assert 'Sorry. That email already exists.' in data['message']
-
-
REST API --.> DB
-
针对“”用户已经存在”的测试脚本 写对应的 REST API。
这里有定义user_param,对参数的类型进行了检测,叫做:Validation
class UsersList(Resource): @api.expect(user_param, validate=True) # new def post(self): print("In UsersList.post()") post_data = request.get_json() username = post_data.get('username') email = post_data.get('email') #----------------------------------------------- response_object = {}
# 要添加新用户 by email,但已经被注册了。 user = User.query.filter_by(email=email).first() if user: response_object['message'] = 'Sorry. That email already exists.' return response_object, 400 db.session.add(User(username=username, email=email)) db.session.commit() response_object = { 'message': f'{email} was added!' } return response_object, 201
增加 HTTP GET 的话,示范如下代码。
先提一句,marshal 的意义,参见:https://www.codenong.com/cs106272440/
person = api.model('Person', { 'name': fields.String( attribute="private_name", default="John", required=True, readonly=True, title="person_title", description="person_description", ), 'age': fields.Integer, }) school = api.model('School', { 'name': fields.String, 'students': fields.List(fields.Nested(person)), 'teachers': fields.List(fields.Nested(person)), }) @api.route('/my-resource/<id>', endpoint='my-resource') class MyResource(Resource): @api.marshal_with(school, as_list=True) # 作为输出model @api.expect(school) # 作为输入model def get(self, id): return {}
-
-
GET single user Route
-
[REST API --> DB]
获取某一个 user id的内容。
class Users(Resource): @api.marshal_with(user_param) def get(self, user_id): user = User.query.filter_by(id=user_id).first() if not user: api.abort(404, f"User {user_id} does not exist") return user, 200 api.add_resource(Users, '/users/<int:user_id>')
-
-
GET all users Route
-
[Client --> REST API]
首先,我们需要先添加若干用户;然后再get获得所有用户。
def test_all_users(test_app, test_database, add_user):
test_database.session.query(User).delete() add_user('michael', 'michael@mherman.org') add_user('fletcher', 'fletcher@notreal.com')
client = test_app.test_client() resp = client.get('/users') data = json.loads(resp.data.decode())
assert resp.status_code == 200 assert len(data) == 2 assert 'michael' in data[0]['username'] assert 'michael@mherman.org' in data[0]['email'] assert 'fletcher' in data[1]['username'] assert 'fletcher@notreal.com' in data[1]['email']
Pytest Commands
一、参考命令
# normal run $ docker-compose exec api python -m pytest "src/tests" # disable warnings $ docker-compose exec api python -m pytest "src/tests" -p no:warnings # run only the last failed tests $ docker-compose exec api python -m pytest "src/tests" --lf # run only the tests with names that match the string expression $ docker-compose exec api python -m pytest "src/tests" -k "config and not test_development_config" # stop the test session after the first failure $ docker-compose exec api python -m pytest "src/tests" -x # enter PDB after first failure then end the test session $ docker-compose exec api python -m pytest "src/tests" -x --pdb # stop the test run after two failures $ docker-compose exec api python -m pytest "src/tests" --maxfail=2 # show local variables in tracebacks $ docker-compose exec api python -m pytest "src/tests" -l # list the 2 slowest tests $ docker-compose exec api python -m pytest "src/tests" --durations=2
End.