如何在python(flask)webapp中模拟服务层以进行单元测试?

Jam*_*ter 5 python unit-testing dependency-injection mocking flask

我正在研究一个webapp in flask并使用服务层来抽象数据库查询和操作远离视图和api路由.有人建议这样可以使测试变得更容易,因为你可以模拟出服务层,但我无法找到一个好方法来做到这一点.举个简单的例子,假设我有三个SQLAlchemy模型:

models.py

class User(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    email = db.Column(db.String)

class Group(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    name = db.Column

class Transaction(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    from_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    to_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    group_id = db.Column(db.Integer, db.ForeignKey('group.id'))
    amount = db.Column(db.Numeric(precision = 2))
Run Code Online (Sandbox Code Playgroud)

用户之间存在用户和组以及交易(代表货币转手).现在我有一个services.py,其中包含一系列函数,例如检查某些用户或组是否存在,检查用户是否是特定组的成员等.我在发送JSON的api路由中使用这些服务在请求中并使用它向db添加事务,类似于:

routes.py

import services

@app.route("/addtrans")
def addtrans():
    # get the values out of the json in the request
    args = request.get_json()
    group_id = args['group_id']
    from_id = args['from']
    to_id = args['to'] 
    amount = args['amount']

    # check that both users exist
    if not services.user_exists(to_id) or not services.user_exists(from_id):
        return "no such users"

    # check that the group exists
    if not services.group_exists(to_id):
        return "no such group"

    # add the transaction to the db
    services.add_transaction(from_id,to_id,group_id,amount)
    return "success"
Run Code Online (Sandbox Code Playgroud)

当我试图模拟这些服务进行测试时,问题出现了.我一直在使用模拟库,我必须修补服务模块中的函数,以便将它们重定向到模拟,如下所示:

mock = Mock()
mock.user_exists.return_value = True
mock.group_exists.return_value = True

@patch("services.user_exists",mock.user_exists)
@patch("services.group_exists",mock.group_exists)
def test_addtrans_route(self):
    assert "success" in routes.addtrans()
Run Code Online (Sandbox Code Playgroud)

由于各种原因,这种感觉很糟糕.一,修补感觉很脏; 二,我不喜欢必须修补我单独使用的每种服务方法(据我所知,没有办法修补整个模块).

我想到了几个方法.

  1. 重新分配routes.services,使其引用我的模拟而不是实际的服务模块,如: routes.services = mymock
  2. 让服务是一个类的方法,它作为关键字参数传递给每个路由,并简单地在测试中传递我的模拟.
  3. 与(2)相同,但使用单个对象.

我在评估这些选项和思考其他选项时遇到了麻烦.进行python web开发的人在测试使用它们的路由时通常会模拟服务吗?

dno*_*zay 7

您可以使用依赖注入控制反转来实现更简单的测试代码.

替换这个:

def addtrans():
    ...
    # check that both users exist
    if not services.user_exists(to_id) or not services.user_exists(from_id):
        return "no such users"
    ...
Run Code Online (Sandbox Code Playgroud)

有:

def addtrans(services=services):
    ...
    # check that both users exist
    if not services.user_exists(to_id) or not services.user_exists(from_id):
        return "no such users"
    ...
Run Code Online (Sandbox Code Playgroud)

发生了什么:

  • 你是将全球化作为本地别名(这不是重点)
  • services希望使用相同的接口来解耦代码.
  • 嘲笑你需要的东西要容易得多

例如:

class MockServices:
    def user_exists(id):
        return True
Run Code Online (Sandbox Code Playgroud)

一些资源: