2013-07-16 77 views
3

我正在使用燒瓶中的web應用程序,並使用服務層將數據庫查詢和操作從視圖和api路線中提取出來。有人建議,這樣可以讓測試變得更容易,因爲你可以模擬出服務層,但是我很難找出一個好辦法來做到這一點。舉一個簡單的例子,假設我有三個型號的SQLAlchemy:如何在python(flask)webapp中模擬服務層進行單元測試?

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

有用戶和組,和交易(代表金錢易手)用戶之間。現在我有一個services.py,它有一堆功能,如檢查某些用戶或組是否存在,檢查用戶是否是特定組的成員等等。我在api路由中使用這些服務發送JSON的請求,並用它來交易添加到數據庫,類似於這樣:

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" 

問題是當我試圖測試模擬出這些服務。我一直在使用mock library,和我有補丁,以便從服務模塊的功能,讓他們被重定向到嘲笑,像這樣:

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開發的人通常在測試使用它們的路由時模擬服務?

回答

6

您可以使用依賴注入反轉控制,從而實現更簡單的測試代碼。

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解耦,而e檢查相同的界面。
  • 嘲笑你需要的東西是很容易

例如爲:

class MockServices: 
    def user_exists(id): 
     return True 

一些資源:

+1

嗯我正在考慮這樣的事情。爲了澄清一下,傳遞給addtrans路由的'services'是一個模塊?所以我會在routes.py的頂部引入「services」,將它作爲默認關鍵字參數傳遞給每個路由,然後在測試中用模擬覆蓋該關鍵字參數? –

+0

由於模塊是一個「對象」,任何具有正確方法/接口的對象都會這樣做。 – dnozay

+0

太棒了,謝謝! –

3

您可以在測試的類級別修補整個服務模塊。然後模擬將被傳遞到每個方法供您修改。

@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。它可以得到一種醜陋,所以謹慎使用。

+0

啊,我明白了,但我更喜歡依賴注入方法。 –

+0

當然,無論你感覺舒服。 – aychedee

0

對於這樣的需求,我也會舉手使用依賴注入。您可以使用Dependency Injector描述使用控件容器(S)的反演應用程序的結構,使它看起來像這樣:

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

希望它有幫助。

相關問題