为什么我的烧瓶测试客户端大约60%的时间失败?

时间:2018-06-24 23:18:54

标签: python flask sqlite sqlalchemy

我已将问题最小化为一个独立的烧瓶应用程序+单元测试。当使用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)

1 个答案:

答案 0 :(得分:1)

如果在提交后在db.session.refresh(t)中添加post(),将会解决此问题。我不知道这样做是否正确(SQLAlchemy非常复杂,我对此有一点经验),但是它表明t对象状态有时没有刷新(有时,因为可能存在争用情况,有时SQLAlchemy 也许获得更多的机器时间并及时提取id,但有时并没有),并且仍然id-属性后某种程度上不存在(对于 flask ,我的意思是,因为sqlite确实存在,但是不会从数据库中获取新状态)。