5

我正在使用烧瓶中的 web 应用程序,并使用服务层将数据库查询和操作从视图和 api 路由中抽象出来。有人建议这使测试更容易,因为您可以模拟服务层,但我无法找到一个好的方法来做到这一点。作为一个简单的例子,假设我有三个 SQLAlchemy 模型:

模型.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))

有用户和群组,以及用户之间的交易(代表货币易手)。现在我有一个services.py,它有很多功能,例如检查某些用户或组是否存在、检查用户是否是特定组的成员等。我在发送 JSON 的 api 路由中使用这些服务在请求中并使用它向数据库添加事务,类似于以下内容:

路线.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"

当我尝试模拟这些服务进行测试时,问题就来了。我一直在使用模拟库,我不得不修补服务模块中的函数,以便将它们重定向到模拟,如下所示:

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()

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

我已经想到了几种解决方法。

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

我无法评估这些选项并考虑其他选项。在测试使用它们的路由时,做 python web 开发的人通常如何模拟服务?

4

4 回答 4

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"
    ...

和:

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"
    ...

发生了什么:

  • 您将全局别名为本地(这不是重点)
  • 您正在将您的代码与services期望相同的接口解耦。
  • 嘲笑你需要的东西要容易得多

例如:

class MockServices:
    def user_exists(id):
        return True

一些资源:

于 2013-07-16T22:43:32.423 回答
4

您可以在测试的类级别修补整个服务模块。然后将模拟传递给每个方法供您修改。

@patch('routes.services')
class MyTestCase(unittest.TestCase):

    def test_my_code_when_services_returns_true(self, mock_services):
        mock_services.user_exists.return_value = True

        self.assertIn('success', routes.addtrans())


    def test_my_code_when_services_returns_false(self, mock_services):
        mock_services.user_exists.return_value = False

        self.assertNotIn('success', routes.addtrans())

对模拟属性的任何访问都会为您提供模拟对象。你可以做一些事情,比如断言一个函数是用mock_services.return_value.some_method.return_value. 它可能会变得有点难看,因此请谨慎使用。

于 2013-07-16T22:47:25.623 回答
0

我也会举手使用依赖注入来满足这种需求。您可以使用依赖注入器来描述应用程序的结构,使用控制容器的反转使其看起来像这样:

"""Example of dependency injection in Python."""

import logging
import sqlite3

import boto3

import example.main
import example.services

import dependency_injector.containers as containers
import dependency_injector.providers as providers


class Core(containers.DeclarativeContainer):
    """IoC container of core component providers."""

    config = providers.Configuration('config')

    logger = providers.Singleton(logging.Logger, name='example')


class Gateways(containers.DeclarativeContainer):
    """IoC container of gateway (API clients to remote services) providers."""

    database = providers.Singleton(sqlite3.connect, Core.config.database.dsn)

    s3 = providers.Singleton(
        boto3.client, 's3',
        aws_access_key_id=Core.config.aws.access_key_id,
        aws_secret_access_key=Core.config.aws.secret_access_key)


class Services(containers.DeclarativeContainer):
    """IoC container of business service providers."""

    users = providers.Factory(example.services.UsersService,
                              db=Gateways.database,
                              logger=Core.logger)

    auth = providers.Factory(example.services.AuthService,
                             db=Gateways.database,
                             logger=Core.logger,
                             token_ttl=Core.config.auth.token_ttl)

    photos = providers.Factory(example.services.PhotosService,
                               db=Gateways.database,
                               s3=Gateways.s3,
                               logger=Core.logger)


class Application(containers.DeclarativeContainer):
    """IoC container of application component providers."""

    main = providers.Callable(example.main.main,
                              users_service=Services.users,
                              auth_service=Services.auth,
                              photos_service=Services.photos)

有了这个,你就有机会在以后覆盖特定的实现:

Services.users.override(providers.Factory(example.services.UsersStub))

希望能帮助到你。

于 2017-03-23T17:56:57.863 回答
0
    @patch("dao.qualcomm_transaction_service.QualcommTransactionService.get_max_qualcomm_id",20) 
def test_lambda_handler(): 
lambda_handler(event, None)

我使用模拟查看您的示例,并且我的方法期望在本地进行 lambda 函数测试时返回 20 get_max_qualcomm_id 我们进行了。但是在达到上述方法时,我得到一个异常 int 类型对象不可调用。请让我知道这里有什么问题。

这是我试图模拟的实际调用方法:

 last_max_id = QualcommTransactionService().get_max_qualcomm_id(self.subscriber_id)
于 2019-01-24T08:49:54.667 回答