Tôi có thể vá một trình trang trí Python trước khi nó kết thúc một hàm không?


81

Tôi có một hàm với trình trang trí mà tôi đang thử kiểm tra với sự trợ giúp của thư viện Python Mock . Tôi muốn sử dụng mock.patchđể thay thế trình trang trí thực bằng một trình trang trí 'bỏ qua' mô phỏng mà chỉ gọi hàm.

Những gì tôi không thể tìm ra là làm thế nào để áp dụng bản vá trước khi trình trang trí thực kết thúc chức năng. Tôi đã thử một vài biến thể khác nhau trên mục tiêu bản vá và sắp xếp lại thứ tự các câu lệnh bản vá và nhập, nhưng không thành công. Có ý kiến ​​gì không?

Câu trả lời:


59

Trình trang trí được áp dụng tại thời điểm xác định hàm. Đối với hầu hết các chức năng, đây là khi mô-đun được tải. (Các hàm được xác định trong các hàm khác có trình trang trí được áp dụng mỗi khi hàm bao quanh được gọi.)

Vì vậy, nếu bạn muốn vá một người trang trí, những gì bạn cần làm là:

  1. Nhập mô-đun có chứa nó
  2. Xác định chức năng trang trí giả
  3. Đặt ví dụ module.decorator = mymockdecorator
  4. Nhập (các) mô-đun sử dụng trình trang trí hoặc sử dụng nó trong mô-đun của riêng bạn

Nếu mô-đun có chứa trình trang trí cũng chứa các chức năng sử dụng nó, thì những chức năng đó đã được trang trí vào thời điểm bạn có thể nhìn thấy chúng và có thể bạn là SOL

Chỉnh sửa để phản ánh các thay đổi đối với Python kể từ khi tôi viết điều này ban đầu: Nếu người trang trí sử dụng functools.wraps()và phiên bản Python đủ mới, bạn có thể tìm hiểu hàm ban đầu bằng cách sử dụng __wrapped__thuộc tính và trang trí lại nó, nhưng điều này không có nghĩa là được đảm bảo và người trang trí bạn muốn thay thế cũng có thể không phải là người trang trí duy nhất được áp dụng.


17
Điều sau làm lãng phí khá nhiều thời gian của tôi: hãy nhớ rằng Python chỉ nhập các mô-đun một lần. Nếu bạn đang chạy một bộ thử nghiệm, cố gắng bắt chước một người trang trí trong một trong các thử nghiệm của bạn và chức năng được trang trí được nhập ở nơi khác, thì việc chế nhạo người trang trí sẽ không có tác dụng.
Paragon

2
sử dụng được xây dựng trong reloadchức năng để tạo lại python nhị phân đang docs.python.org/2/library/functions.html#reload và monkeypatch trang trí của bạn
IxDay

2
Chạy vào vấn đề được báo cáo bởi @Paragon và khắc phục nó bằng cách vá trình trang trí của tôi trong thư mục thử nghiệm __init__. Điều đó đảm bảo rằng bản vá đã được tải trước bất kỳ tệp thử nghiệm nào. Chúng tôi có một thư mục thử nghiệm riêng biệt nên chiến lược này phù hợp với chúng tôi, nhưng điều này có thể không hiệu quả với mọi bố cục thư mục.
claytond,

4
Sau khi đọc cái này vài lần, tôi vẫn còn bối rối. Điều này cần một ví dụ mã!
ritratt

@claytond Cảm ơn giải pháp của bạn đã phù hợp với tôi vì tôi có một thư mục thử nghiệm riêng biệt!
Srivathsa ngày

55

Cần lưu ý rằng một số câu trả lời ở đây sẽ vá trình trang trí cho toàn bộ phiên kiểm tra hơn là một phiên bản kiểm tra đơn lẻ; mà có thể không mong muốn. Đây là cách vá lỗi trang trí chỉ tồn tại qua một lần kiểm tra duy nhất.

Đơn vị của chúng tôi sẽ được kiểm tra với trình trang trí không mong muốn:

# app/uut.py

from app.decorators import func_decor

@func_decor
def unit_to_be_tested():
    # Do stuff
    pass

Từ mô-đun decorator:

# app/decorators.py

def func_decor(func):
    def inner(*args, **kwargs):
        print "Do stuff we don't want in our test"
        return func(*args, **kwargs)
    return inner

Vào thời điểm thử nghiệm của chúng tôi được thu thập trong quá trình chạy thử nghiệm, trình trang trí không mong muốn đã được áp dụng cho đơn vị đang thử nghiệm của chúng tôi (vì điều đó xảy ra tại thời điểm nhập). Để loại bỏ điều đó, chúng tôi sẽ cần thay thế thủ công trình trang trí trong mô-đun của trình trang trí và sau đó nhập lại mô-đun chứa UUT của chúng tôi.

Mô-đun thử nghiệm của chúng tôi:

#  test_uut.py

from unittest import TestCase
from app import uut  # Module with our thing to test
from app import decorators  # Module with the decorator we need to replace
import imp  # Library to help us reload our UUT module
from mock import patch


class TestUUT(TestCase):
    def setUp(self):
        # Do cleanup first so it is ready if an exception is raised
        def kill_patches():  # Create a cleanup callback that undoes our patches
            patch.stopall()  # Stops all patches started with start()
            imp.reload(uut)  # Reload our UUT module which restores the original decorator
        self.addCleanup(kill_patches)  # We want to make sure this is run so we do this in addCleanup instead of tearDown

        # Now patch the decorator where the decorator is being imported from
        patch('app.decorators.func_decor', lambda x: x).start()  # The lambda makes our decorator into a pass-thru. Also, don't forget to call start()          
        # HINT: if you're patching a decor with params use something like:
        # lambda *x, **y: lambda f: f
        imp.reload(uut)  # Reloads the uut.py module which applies our patched decorator

Lệnh gọi lại dọn dẹp, kill_patches, khôi phục trình trang trí ban đầu và áp dụng lại nó cho đơn vị chúng tôi đang thử nghiệm. Bằng cách này, bản vá của chúng tôi chỉ tồn tại qua một thử nghiệm duy nhất thay vì toàn bộ phiên - đó chính xác là cách mà bất kỳ bản vá nào khác sẽ hoạt động. Ngoài ra, kể từ khi dọn dẹp cuộc gọi patch.stopall (), chúng ta có thể bắt đầu bất kỳ bản vá nào khác trong setUp () mà chúng ta cần và chúng sẽ được dọn dẹp tất cả ở một nơi.

Điều quan trọng cần hiểu về phương pháp này là cách tải lại sẽ ảnh hưởng đến mọi thứ. Nếu một mô-đun mất quá nhiều thời gian hoặc có logic chạy khi nhập, bạn có thể chỉ cần nhún và kiểm tra trình trang trí như một phần của đơn vị. :( Hy vọng rằng mã của bạn được viết tốt hơn thế. Đúng không?

Nếu ai đó không quan tâm liệu bản vá có được áp dụng cho toàn bộ phiên kiểm tra hay không , thì cách dễ nhất để làm điều đó là ngay ở đầu tệp kiểm tra:

# test_uut.py

from mock import patch
patch('app.decorators.func_decor', lambda x: x).start()  # MUST BE BEFORE THE UUT GETS IMPORTED ANYWHERE!

from app import uut

Đảm bảo vá tệp bằng trình trang trí thay vì phạm vi cục bộ của UUT và bắt đầu bản vá trước khi nhập đơn vị với trình trang trí.

Điều thú vị là ngay cả khi bản vá bị dừng, tất cả các tệp đã được nhập vẫn sẽ có bản vá được áp dụng cho trình trang trí, điều này ngược lại với tình huống mà chúng tôi đã bắt đầu. Lưu ý rằng phương pháp này sẽ vá bất kỳ tệp nào khác trong quá trình chạy thử nghiệm được nhập sau đó - ngay cả khi chúng không tự khai báo bản vá.


1
user2859458, điều này đã giúp tôi đáng kể. Câu trả lời được chấp nhận là tốt, nhưng điều này nói với tôi theo cách có ý nghĩa và bao gồm nhiều trường hợp sử dụng mà bạn có thể muốn một cái gì đó hơi khác một chút.
Malcolm Jones

1
Cảm ơn bạn vì phản hồi này! Trong trường hợp này rất hữu ích cho người khác, tôi đã thực hiện một phần mở rộng của bản vá sẽ vẫn làm việc như một người quản lý bối cảnh và làm quá trình tải dành cho bạn: gist.github.com/Geekfish/aa43368ceade131b8ed9c822d2163373
Geekfish

12

Khi lần đầu tiên gặp phải vấn đề này, tôi thường gồng mình lên hàng giờ liền. Tôi đã tìm thấy một cách dễ dàng hơn nhiều để xử lý điều này.

Điều này sẽ hoàn toàn vượt qua trình trang trí, giống như mục tiêu thậm chí không được trang trí ngay từ đầu.

Điều này được chia thành hai phần. Tôi đề nghị đọc bài viết sau.

http://alexmarandon.com/articles/python_mock_gotchas/

Hai Gotcha mà tôi liên tục gặp phải:

1.) Giả lập Trình trang trí trước khi nhập chức năng / mô-đun của bạn.

Các trình trang trí và chức năng được xác định tại thời điểm mô-đun được tải. Nếu bạn không mô phỏng trước khi nhập, nó sẽ bỏ qua mô hình đó. Sau khi tải, bạn phải thực hiện một mock.patch.object kỳ lạ, điều này thậm chí còn khó chịu hơn.

2.) Đảm bảo rằng bạn đang mô phỏng đường dẫn chính xác đến người trang trí.

Hãy nhớ rằng bản vá của trình trang trí mà bạn đang chế nhạo dựa trên cách mô-đun của bạn tải trình trang trí, chứ không phải cách thử nghiệm của bạn tải trình trang trí. Đây là lý do tại sao tôi khuyên bạn nên luôn sử dụng đường dẫn đầy đủ để nhập. Điều này làm cho mọi thứ dễ dàng hơn rất nhiều để kiểm tra.

Các bước:

1.) Chức năng Mock:

from functools import wraps

def mock_decorator(*args, **kwargs):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            return f(*args, **kwargs)
        return decorated_function
    return decorator

2.) Chế giễu người trang trí:

2a.) Đường dẫn bên trong với.

with mock.patch('path.to.my.decorator', mock_decorator):
     from mymodule import myfunction

2b.) Bản vá ở đầu tệp hoặc trong TestCase.setUp

mock.patch('path.to.my.decorator', mock_decorator).start()

Một trong hai cách này sẽ cho phép bạn nhập chức năng của mình bất cứ lúc nào trong TestCase hoặc phương pháp / trường hợp thử nghiệm của nó.

from mymodule import myfunction

2.) Sử dụng một chức năng riêng biệt như một tác dụng phụ của mock.patch.

Bây giờ bạn có thể sử dụng mock_decorator cho mỗi trang trí mà bạn muốn mô phỏng. Bạn sẽ phải giả từng người trang trí riêng biệt, vì vậy hãy để ý những người bạn bỏ sót.


1
Bài đăng trên blog mà bạn trích dẫn đã giúp tôi hiểu điều này tốt hơn nhiều!
ritratt

2

Những điều sau đây đã làm việc cho tôi:

  1. Loại bỏ câu lệnh nhập tải mục tiêu thử nghiệm.
  2. Vá trình trang trí khi khởi động thử nghiệm như đã áp dụng ở trên.
  3. Gọi importlib.import_module () ngay sau khi vá để tải mục tiêu thử nghiệm.
  4. Chạy thử nghiệm bình thường.

Nó làm việc như một say mê.


1

Chúng tôi đã cố gắng bắt chước một trình trang trí đôi khi nhận được một tham số khác như một chuỗi và một số lần thì không, ví dụ:

@myDecorator('my-str')
def function()

OR

@myDecorator
def function()

Nhờ một trong những câu trả lời ở trên, chúng tôi đã viết một hàm giả và vá trình trang trí bằng hàm giả này:

from mock import patch

def mock_decorator(f):

    def decorated_function(g):
        return g

    if callable(f): # if no other parameter, just return the decorated function
        return decorated_function(f)
    return decorated_function # if there is a parametr (eg. string), ignore it and return the decorated function

patch('path.to.myDecorator', mock_decorator).start()

from mymodule import myfunction

Lưu ý rằng ví dụ này phù hợp với trình trang trí không chạy chức năng trang trí, chỉ thực hiện một số nội dung trước khi chạy thực tế. Trong trường hợp trình trang trí cũng chạy hàm được trang trí, và do đó nó cần chuyển các tham số của hàm, thì hàm mock_decorator phải khác một chút.

Hy vọng điều này sẽ giúp những người khác ...


0

Có thể bạn có thể áp dụng một trình trang trí khác vào các định nghĩa của tất cả các trình trang trí của bạn về cơ bản kiểm tra một số biến cấu hình để xem liệu chế độ thử nghiệm có được sử dụng hay không.
Nếu có, nó sẽ thay thế người trang trí mà nó đang trang trí bằng một người trang trí giả không có tác dụng gì.
Nếu không, nó cho phép người trang trí này thông qua.


0

Ý tưởng

Điều này nghe có vẻ hơi kỳ quặc nhưng người ta có thể vá sys.path, với một bản sao của chính nó và thực hiện nhập trong phạm vi của chức năng thử nghiệm. Đoạn mã sau đây cho thấy khái niệm.

from unittest.mock import patch
import sys

@patch('sys.modules', sys.modules.copy())
def testImport():
 oldkeys = set(sys.modules.keys())
 import MODULE
 newkeys = set(sys.modules.keys())
 print((newkeys)-(oldkeys))

oldkeys = set(sys.modules.keys())
testImport()                       -> ("MODULE") # Set contains MODULE
newkeys = set(sys.modules.keys())
print((newkeys)-(oldkeys))         -> set()      # An empty set

MODULEsau đó có thể được thay thế bằng mô-đun bạn đang thử nghiệm. (Điều này hoạt động trong Python 3.6 với ví dụ được MODULEthay thế bằng xml)

OP

Đối với trường hợp của bạn, giả sử chức năng trang trí nằm trong mô-đun prettyvà chức năng trang trí nằm trong present, sau đó bạn sẽ vá pretty.decoratorbằng cách sử dụng máy móc giả và thay thế MODULEbằng present. Một cái gì đó như sau sẽ hoạt động (Chưa được kiểm tra).

class TestDecorator (unittest.TestCase): ...

  @patch(`pretty.decorator`, decorator)
  @patch(`sys.path`, sys.path.copy())
  def testFunction(self, decorator) :
   import present
   ...

Giải trình

Điều này hoạt động bằng cách cung cấp "sạch" sys.pathcho mỗi chức năng kiểm tra, sử dụng bản sao dòng điện sys.pathcủa mô-đun kiểm tra. Bản sao này được thực hiện khi mô-đun được phân tích cú pháp lần đầu tiên đảm bảo tính nhất quán sys.pathcho tất cả các bài kiểm tra.

Sắc thái

Tuy nhiên, có một vài hàm ý. Nếu khung thử nghiệm chạy nhiều mô-đun thử nghiệm trong cùng một phiên python, bất kỳ mô-đun thử nghiệm nào nhập MODULEtoàn cục sẽ phá vỡ bất kỳ mô-đun thử nghiệm nào nhập nó cục bộ. Điều này buộc người ta phải thực hiện nhập cục bộ ở mọi nơi. Nếu khung chạy từng mô-đun thử nghiệm trong một phiên python riêng thì điều này sẽ hoạt động. Tương tự, bạn không thể nhập MODULEtoàn cầu trong một mô-đun thử nghiệm mà bạn đang nhập MODULEcục bộ.

Việc nhập nội địa phải được thực hiện cho mỗi chức năng thử nghiệm trong một lớp con của unittest.TestCase. Có lẽ có thể áp dụng điều này cho unittest.TestCaselớp con trực tiếp làm cho một nhập cụ thể của mô-đun có sẵn cho tất cả các chức năng kiểm tra trong lớp.

Đã xây dựng

Những lỗi builtinnhập khẩu sẽ tìm thấy thay thế MODULEbằng sys, osv.v. sẽ không thành công, vì chúng sẽ được đọc sys.pathkhi bạn cố gắng sao chép nó. Mẹo ở đây là gọi Python với nhập nội trang bị vô hiệu hóa, tôi nghĩ python -X test.pysẽ làm được nhưng tôi quên cờ thích hợp (Xem python --help). Những thứ này sau đó có thể được nhập khẩu trong nước bằng import builtinsIIRC.


0

Để vá một trình trang trí, bạn cần nhập hoặc tải lại mô-đun sử dụng trình trang trí đó sau khi vá nó HOẶC xác định lại toàn bộ tham chiếu của mô-đun đến trình trang trí đó.

Trình trang trí được áp dụng tại thời điểm một mô-đun được nhập. Đây là lý do tại sao nếu bạn nhập một mô-đun sử dụng trình trang trí mà bạn muốn vá ở đầu tệp của mình và cố gắng vá nó sau này mà không cần tải lại, thì bản vá sẽ không có tác dụng.

Dưới đây là một ví dụ về cách đầu tiên được đề cập để thực hiện việc này - tải lại một mô-đun sau khi vá một trình trang trí mà nó sử dụng:

import moduleA
...

  # 1. patch the decorator
  @patch('decoratorWhichIsUsedInModuleA', examplePatchValue)
  def setUp(self)
    # 2. reload the module which uses the decorator
    reload(moduleA)

  def testFunctionA(self):
    # 3. tests...
    assert(moduleA.functionA()...

Tài liệu tham khảo hữu ích:


-2

cho @lru_cache (max_size = 1000)


class MockedLruCache(object):

def __init__(self, maxsize=0, timeout=0):
    pass

def __call__(self, func):
    return func

cache.LruCache = MockedLruCache

nếu sử dụng decorator không có params, bạn nên:

def MockAuthenticated(func):
    return func

from tornado import web web.authenticated = MockAuthenticated


1
Tôi thấy rất nhiều vấn đề trong câu trả lời này. Đầu tiên (và lớn hơn) là bạn không thể truy cập vào chức năng ban đầu nếu nó được trang trí (đó là vấn đề OP). Hơn nữa, bạn không gỡ bỏ bản vá sau khi kiểm tra xong và điều đó có thể gây ra sự cố khi bạn chạy nó trong bộ thử nghiệm.
Michele d'Amico
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.