我已将问题最小化为一个独立的烧瓶应用程序+单元测试。当使用pytest app.py
运行此命令时,大约有一半的时间(运行50次中的29次)失败,并出现以下错误:
E werkzeug.routing.BuildError: Could not build url for endpoint 'thing' with values ['_sa_instance_state']. Did you forget to specify values ['id']?
与此有关的令人沮丧的部分是,在post()
方法中添加调试语句使其始终通过(请参见下面的评论)。
这感觉像是框架中某个地方的竞争条件。 SQLAlchemy是否产生了一个线程来执行提交并更新t.id
?
我可以通过在评论的位置执行del t.id
来强制其失败(确认错误是由于缺少t.id引起的)。我可以通过在同一位置执行t.id = 999
来强制它通过。
我在这里做错了什么吗?或者这是其中一个软件包的错误?
我正在运行python 3.5.2,而我的requirements.txt是:
Flask==1.0.2
Flask-RESTful==0.3.6
Flask-SQLAlchemy==2.3.2
Jinja2==2.10
pytest==3.2.2
pytest-repeat==0.4.1
SQLAlchemy==1.2.8
Werkzeug==0.14.1
值得注意的是,大多数此类软件包的早期版本(烧瓶0.12,sqlalchemy 1.1.14等)也失败了。
还可能值得注意的是,当使用pytest --count=20 app.py
运行时,它将始终通过或失败全部计数,即20次通过或20次失败。但是大约一半的运行仍将失败。
这是应用程序:
#!/usr/bin/env python3
import json
from flask import Flask
from flask_restful import Api, Resource, fields, marshal, reqparse
from flask_sqlalchemy import SQLAlchemy
import pytest
app = Flask(__name__)
api = Api(app)
db = SQLAlchemy(app)
class Thing(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
thing_fields = {
'name': fields.String,
'uri': fields.Url('thing'),
}
class ThingListAPI(Resource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
self.reqparse.add_argument('name', type=str, location='json')
super().__init__()
def post(self):
args = self.reqparse.parse_args()
t = Thing(name=args['name'])
db.session.add(t)
db.session.commit()
### <<< at this point inserting pretty much any statement
### will make the test pass >>>
return {'thing': marshal(t, thing_fields)}, 201
class ThingAPI(Resource):
def get(self, id):
pass
api.add_resource(ThingListAPI, '/things', endpoint='things')
api.add_resource(ThingAPI, '/things/<int:id>', endpoint='thing')
@pytest.fixture
def stub_app():
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
client = app.test_client()
db.create_all()
yield client
db.drop_all()
def test_thing_post(stub_app):
resp = stub_app.post('/things', data=json.dumps({'name': 'stuff'}),
content_type='application/json')
assert(resp.status_code == 201)
答案 0 :(得分:1)
如果在提交后在db.session.refresh(t)
中添加post()
,将会解决此问题。我不知道这样做是否正确(SQLAlchemy非常复杂,我对此有一点经验),但是它表明t
对象状态有时没有刷新(有时,因为可能存在争用情况,有时SQLAlchemy 也许获得更多的机器时间并及时提取id,但有时并没有),并且仍然id
-属性后某种程度上不存在(对于 flask ,我的意思是,因为sqlite确实存在,但是不会从数据库中获取新状态)。