unittest.mock --- bắt đầu

Added in version 3.3.

Sử dụng mô phỏng

Phương pháp vá giả

Các cách sử dụng phổ biến cho các đối tượng Mock bao gồm:

  • Phương pháp vá lỗi

  • Ghi lại các cuộc gọi phương thức trên các đối tượng

Bạn có thể muốn thay thế một phương thức trên một đối tượng để kiểm tra xem nó có được một phần khác của hệ thống gọi với các đối số chính xác hay không:

>>> real = SomeClass()
>>> real.method = MagicMock(name='method')
>>> real.method(3, 4, 5, key='value')
<MagicMock name='method()' id='...'>

Khi mô hình của chúng tôi đã được sử dụng (real.method trong ví dụ này), nó có các phương thức và thuộc tính cho phép bạn đưa ra xác nhận về cách nó được sử dụng.

Ghi chú

Trong hầu hết các ví dụ này, các lớp MockMagicMock có thể hoán đổi cho nhau. Vì MagicMock là lớp có khả năng cao hơn nên nó trở thành một lớp hợp lý để sử dụng theo mặc định.

Khi mô hình đã được gọi, thuộc tính called của nó được đặt thành True. Quan trọng hơn, chúng ta có thể sử dụng phương thức assert_called_with() hoặc assert_called_once_with() để kiểm tra xem nó có được gọi với các đối số chính xác hay không.

Ví dụ này kiểm tra việc gọi ProductionClass().method có dẫn đến lệnh gọi phương thức something hay không:

>>> class ProductionClass:
...     def method(self):
...         self.something(1, 2, 3)
...     def something(self, a, b, c):
...         pass
...
>>> real = ProductionClass()
>>> real.something = MagicMock()
>>> real.method()
>>> real.something.assert_called_once_with(1, 2, 3)

Mô phỏng các lệnh gọi phương thức trên một đối tượng

Trong ví dụ trước, chúng tôi đã vá một phương thức trực tiếp trên một đối tượng để kiểm tra xem nó có được gọi chính xác hay không. Một trường hợp sử dụng phổ biến khác là chuyển một đối tượng vào một phương thức (hoặc một phần nào đó của hệ thống đang được kiểm tra) và sau đó kiểm tra xem nó có được sử dụng đúng cách hay không.

Zz000zz đơn giản bên dưới có phương thức closer. Nếu nó được gọi với một đối tượng thì nó sẽ gọi close trên đó.

>>> class ProductionClass:
...     def closer(self, something):
...         something.close()
...

Vì vậy, để kiểm tra nó, chúng ta cần truyền vào một đối tượng bằng phương thức close và kiểm tra xem nó có được gọi chính xác hay không.

>>> real = ProductionClass()
>>> mock = Mock()
>>> real.closer(mock)
>>> mock.close.assert_called_with()

Chúng tôi không phải thực hiện bất kỳ công việc nào để cung cấp phương thức 'đóng' trên mô hình của mình. Truy cập đóng sẽ tạo ra nó. Vì vậy, nếu 'đóng' chưa được gọi thì việc truy cập vào nó trong thử nghiệm sẽ tạo ra nó, nhưng assert_called_with() sẽ đưa ra một ngoại lệ lỗi.

Lớp học chế nhạo

Trường hợp sử dụng phổ biến là mô phỏng các lớp được khởi tạo bằng mã của bạn đang được thử nghiệm. Khi bạn vá một lớp, lớp đó sẽ được thay thế bằng một bản mô phỏng. Các phiên bản được tạo bởi calling the class. Điều này có nghĩa là bạn truy cập vào "phiên bản giả" bằng cách xem giá trị trả về của lớp được mô phỏng.

Trong ví dụ bên dưới, chúng ta có hàm some_function khởi tạo Foo và gọi một phương thức trên đó. Lệnh gọi tới patch() thay thế lớp Foo bằng một bản mô phỏng. Phiên bản Foo là kết quả của việc gọi mô hình, vì vậy nó được định cấu hình bằng cách sửa đổi mô hình return_value.

>>> def some_function():
... instance = module.Foo()
... trả về instance.method()
...
>>> với patch('module.Foo') làm  hình:
... instance = mock.return_value
... instance.method.return_value = 'kết quả'
... kết quả = some_function()
... khẳng định kết quả == 'kết quả'

Đặt tên cho mô hình của bạn

Việc đặt tên cho mô hình của bạn có thể hữu ích. Tên được hiển thị trong phần mô phỏng của mô hình và có thể hữu ích khi mô hình xuất hiện trong thông báo kiểm tra thất bại. Tên này cũng được truyền tới các thuộc tính hoặc phương thức của mô hình:

>>> mock = MagicMock(name='foo')
>>> mock
<MagicMock name='foo' id='...'>
>>> mock.method
<MagicMock name='foo.method' id='...'>

Theo dõi tất cả các cuộc gọi

Thường thì bạn muốn theo dõi nhiều hơn một cuộc gọi đến một phương thức. Thuộc tính mock_calls ghi lại tất cả các lệnh gọi đến thuộc tính con của mô hình - và cả thuộc tính con của chúng.

>>> mock = MagicMock()
>>> mock.method()
<MagicMock name='mock.method()' id='...'>
>>> mock.attribute.method(10, x=53)
<MagicMock name='mock.attribute.method()' id='...'>
>>> mock.mock_calls
[call.method(), call.attribute.method(10, x=53)]

Nếu bạn đưa ra một xác nhận về mock_calls và bất kỳ phương thức không mong muốn nào đã được gọi thì xác nhận đó sẽ thất bại. Điều này rất hữu ích vì ngoài việc xác nhận rằng các cuộc gọi bạn mong đợi đã được thực hiện, bạn cũng đang kiểm tra xem chúng có được thực hiện theo đúng thứ tự và không có cuộc gọi bổ sung nào không:

Bạn sử dụng đối tượng call để xây dựng danh sách so sánh với mock_calls:

>>> expected = [call.method(), call.attribute.method(10, x=53)]
>>> mock.mock_calls == expected
True

Tuy nhiên, các tham số cho các cuộc gọi trả về mô phỏng không được ghi lại, điều đó có nghĩa là không thể theo dõi các cuộc gọi lồng nhau trong đó các tham số được sử dụng để tạo tổ tiên là quan trọng:

>>> m = Mock()
>>> m.factory(important=True).deliver()
<Mock name='mock.factory().deliver()' id='...'>
>>> m.mock_calls[-1] == call.factory(important=False).deliver()
True

Đặt giá trị và thuộc tính trả về

Việc đặt các giá trị trả về trên một đối tượng giả rất dễ dàng:

>>> mock = Mock()
>>> mock.return_value = 3
>>> mock()
3

Tất nhiên bạn có thể làm tương tự với các phương thức trên mô hình:

>>> mock = Mock()
>>> mock.method.return_value = 3
>>> mock.method()
3

Giá trị trả về cũng có thể được đặt trong hàm tạo:

>>> mock = Mock(return_value=3)
>>> mock()
3

Nếu bạn cần cài đặt thuộc tính trên mô hình của mình, chỉ cần thực hiện:

>>> mock = Mock()
>>> mock.x = 3
>>> mock.x
3

Đôi khi bạn muốn mô phỏng một tình huống phức tạp hơn, chẳng hạn như mock.connection.cursor().execute("SELECT 1"). Nếu chúng ta muốn lệnh gọi này trả về một danh sách thì chúng ta phải định cấu hình kết quả của lệnh gọi lồng nhau.

Chúng ta có thể sử dụng call để xây dựng tập hợp các cuộc gọi theo "cuộc gọi nối tiếp" như thế này để dễ dàng xác nhận sau đó:

>>> mock = Mock()
>>> cursor = mock.connection.cursor.return_value
>>> cursor.execute.return_value = ['foo']
>>> mock.connection.cursor().execute("SELECT 1")
['foo']
>>> expected = call.connection.cursor().execute("SELECT 1").call_list()
>>> mock.mock_calls
[call.connection.cursor(), call.connection.cursor().execute('SELECT 1')]
>>> mock.mock_calls == expected
True

Chính lệnh gọi tới .call_list() sẽ biến đối tượng cuộc gọi của chúng ta thành một danh sách các cuộc gọi đại diện cho các cuộc gọi được xâu chuỗi.

Tăng ngoại lệ với mô phỏng

Một thuộc tính hữu ích là side_effect. Nếu bạn đặt điều này thành một lớp hoặc phiên bản ngoại lệ thì ngoại lệ đó sẽ được đưa ra khi mô hình được gọi.

>>> mock = Mock(side_effect=Exception('Boom!'))
>>> mock()
Traceback (most recent call last):
  ...
Exception: Boom!

Các hàm tác dụng phụ và các lần lặp

side_effect cũng có thể được đặt thành một hàm hoặc một iterable. Trường hợp sử dụng side_effect dưới dạng iterable là nơi mô phỏng của bạn sẽ được gọi nhiều lần và bạn muốn mỗi lệnh gọi trả về một giá trị khác nhau. Khi bạn đặt side_effect thành một iterable, mọi lệnh gọi tới mock sẽ trả về giá trị tiếp theo từ iterable:

>>> mock = MagicMock(side_effect=[4, 5, 6])
>>> mock()
4
>>> mock()
5
>>> mock()
6

Đối với các trường hợp sử dụng nâng cao hơn, chẳng hạn như thay đổi linh hoạt các giá trị trả về tùy thuộc vào nội dung mô phỏng được gọi, side_effect có thể là một hàm. Hàm sẽ được gọi với các đối số giống như hàm giả. Bất cứ điều gì hàm trả về là những gì cuộc gọi trả về:

>>> vals = {(1, 2): 1, (2, 3): 2}
>>> def side_effect(*args):
...     return vals[args]
...
>>> mock = MagicMock(side_effect=side_effect)
>>> mock(1, 2)
1
>>> mock(2, 3)
2

Mô phỏng các trình vòng lặp không đồng bộ

Vì Python 3.8, AsyncMockMagicMock có hỗ trợ mô phỏng Trình lặp không đồng bộ thông qua __aiter__. Thuộc tính return_value của __aiter__ có thể được sử dụng để đặt các giá trị trả về được sử dụng cho lần lặp.

>>> mock = MagicMock()  # AsyncMock also works here
>>> mock.__aiter__.return_value = [1, 2, 3]
>>> async def main():
...     return [i async for i in mock]
...
>>> asyncio.run(main())
[1, 2, 3]

Mô phỏng trình quản lý bối cảnh không đồng bộ

Kể từ Python 3.8, AsyncMockMagicMock có hỗ trợ mô phỏng Trình quản lý bối cảnh không đồng bộ thông qua __aenter____aexit__. Theo mặc định, __aenter____aexit__ là các phiên bản AsyncMock trả về hàm không đồng bộ.

>>> class AsyncContextManager:
...     async def __aenter__(self):
...         return self
...     async def __aexit__(self, exc_type, exc, tb):
...         pass
...
>>> mock_instance = MagicMock(AsyncContextManager())  # AsyncMock also works here
>>> async def main():
...     async with mock_instance as result:
...         pass
...
>>> asyncio.run(main())
>>> mock_instance.__aenter__.assert_awaited_once()
>>> mock_instance.__aexit__.assert_awaited_once()

Tạo mô hình từ đối tượng hiện có

Một vấn đề với việc sử dụng quá nhiều chế độ mô phỏng là nó kết hợp các thử nghiệm của bạn với việc triển khai các mô hình thay vì mã thực của bạn. Giả sử bạn có một lớp triển khai some_method. Trong quá trình kiểm tra cho một lớp khác, bạn cung cấp một bản mô phỏng của đối tượng này mà also cung cấp cho some_method. Nếu sau này bạn cấu trúc lại lớp đầu tiên để nó không còn some_method nữa - thì các bài kiểm tra của bạn sẽ tiếp tục vượt qua ngay cả khi mã của bạn hiện đã bị hỏng!

Mock cho phép bạn cung cấp một đối tượng làm thông số kỹ thuật cho mô hình bằng cách sử dụng đối số từ khóa spec. Việc truy cập các phương thức/thuộc tính trên mô hình không tồn tại trên đối tượng đặc tả của bạn sẽ ngay lập tức gây ra lỗi thuộc tính. Nếu bạn thay đổi cách triển khai đặc tả của mình thì các thử nghiệm sử dụng lớp đó sẽ bắt đầu thất bại ngay lập tức mà bạn không cần phải khởi tạo lớp trong các thử nghiệm đó.

>>> mock = Mock(spec=SomeClass)
>>> mock.old_method()
Traceback (most recent call last):
   ...
AttributeError: Mock object has no attribute 'old_method'. Did you mean: 'class_method'?

Việc sử dụng thông số kỹ thuật cũng cho phép kết hợp thông minh hơn các lệnh gọi được thực hiện với mô hình, bất kể một số tham số được chuyển dưới dạng đối số vị trí hay đối số được đặt tên:

>>> def f(a, b, c): đạt
...
>>>  phỏng =  phỏng(spec=f)
>>> giả (1, 2, 3)
<Tên giả='mock()' id='140161580456576'>
>>> mock.assert_gọi_with(a=1, b=2, c=3)

Nếu bạn muốn tính năng kết hợp thông minh hơn này cũng hoạt động với các lệnh gọi phương thức trên mô hình, bạn có thể sử dụng auto-speccing.

Nếu bạn muốn một dạng đặc tả mạnh hơn nhằm ngăn chặn việc thiết lập các thuộc tính tùy ý cũng như việc lấy chúng thì bạn có thể sử dụng spec_set thay vì spec.

Sử dụng side_effect để trả về mỗi nội dung tệp

mock_open() được sử dụng để vá phương thức open(). side_effect có thể được sử dụng để trả về một đối tượng Mock mới cho mỗi lệnh gọi. Điều này có thể được sử dụng để trả về các nội dung khác nhau cho mỗi tệp được lưu trữ trong từ điển

DEFAULT = "mặc định"
data_dict = {"file1": "data1",
             "file2": "data2"}

def open_side_effect(tên):
    trả về mock_open(read_data=data_dict.get(name, DEFAULT))()

với bản  ("buildins.open", side_effect=open_side_effect):
    với open("file1")  file1:
        khẳng định file1.read() == "data1"

    với open("file2")  file2:
        khẳng định file2.read() == "data2"

    với open("file3")  file2:
        khẳng định file2.read() == "mặc định"

Trang trí bản vá

Ghi chú

Với patch(), điều quan trọng là bạn vá các đối tượng trong không gian tên nơi chúng được tra cứu. Điều này thường đơn giản nhưng để có hướng dẫn nhanh, hãy đọc where to patch.

Một nhu cầu phổ biến trong các thử nghiệm là vá một thuộc tính lớp hoặc một thuộc tính mô-đun, ví dụ như vá một phần dựng sẵn hoặc vá một lớp trong mô-đun để kiểm tra xem nó đã được khởi tạo chưa. Các mô-đun và lớp có tính chất toàn cầu một cách hiệu quả, vì vậy việc vá lỗi trên chúng phải được hoàn tác sau khi thử nghiệm, nếu không bản vá sẽ tiếp tục tồn tại trong các thử nghiệm khác và gây ra sự cố khó chẩn đoán.

mock cung cấp ba trình trang trí thuận tiện cho việc này: patch(), patch.object()patch.dict(). patch lấy một chuỗi duy nhất có dạng package.module.Class.attribute để chỉ định thuộc tính bạn đang vá. Nó cũng tùy ý nhận một giá trị mà bạn muốn thay thế thuộc tính (hoặc lớp hoặc bất cứ thứ gì). 'patch.object' lấy một đối tượng và tên của thuộc tính bạn muốn vá, cộng với giá trị tùy chọn để vá nó.

patch.object:

>>> gốc = SomeClass.attribute
>>> @patch.object(SomeClass, 'thuộc tính', canh gác.attribute)
... kiểm tra chắc chắn():
... khẳng định SomeClass.attribute == canh gác.attribute
...
>>> kiểm tra()
>>> khẳng định SomeClass.attribute == bản gốc

>>> @patch('package.module.attribute', sendinel.attribute)
... kiểm tra chắc chắn():
... từ thuộc tính nhập package.module
... thuộc tính khẳng định  canh gác.attribute
...
>>> kiểm tra()

Nếu bạn đang vá một mô-đun (bao gồm builtins) thì hãy sử dụng patch() thay vì patch.object():

>>> mock = MagicMock(return_value=sentinel.file_handle)
>>> with patch('builtins.open', mock):
...     handle = open('filename', 'r')
...
>>> mock.assert_called_with('filename', 'r')
>>> assert handle == sentinel.file_handle, "incorrect file handle returned"

Tên mô-đun có thể được 'chấm', ở dạng package.module nếu cần:

>>> @patch('package.module.ClassName.attribute', canh gác.attribute)
... kiểm tra chắc chắn():
... từ nhập package.module ClassName
... khẳng định ClassName.attribute == canh gác.attribute
...
>>> kiểm tra()

Một mô hình hay là thực sự tự trang trí các phương pháp thử nghiệm:

>>> class MyTest(unittest.TestCase):
...     @patch.object(SomeClass, 'attribute', sentinel.attribute)
...     def test_something(self):
...         self.assertEqual(SomeClass.attribute, sentinel.attribute)
...
>>> original = SomeClass.attribute
>>> MyTest('test_something').test_something()
>>> assert SomeClass.attribute == original

Nếu bạn muốn vá bằng Mock, bạn có thể sử dụng patch() chỉ với một đối số (hoặc patch.object() với hai đối số). Bản mô phỏng sẽ được tạo cho bạn và chuyển vào hàm/phương thức kiểm tra:

>>> class MyTest(unittest.TestCase):
...     @patch.object(SomeClass, 'static_method')
...     def test_something(self, mock_method):
...         SomeClass.static_method()
...         mock_method.assert_called_with()
...
>>> MyTest('test_something').test_something()

Bạn có thể xếp chồng nhiều công cụ trang trí bản vá bằng cách sử dụng mẫu này:

>>> lớp MyTest(unittest.TestCase):
... @patch('package.module.ClassName1')
... @patch('package.module.ClassName2')
... def test_something(self, MockClass2, MockClass1):
... self.assertIs(package.module.ClassName1, MockClass1)
... self.assertIs(package.module.ClassName2, MockClass2)
...
>>> MyTest('test_something').test_something()

Khi bạn lồng các trình trang trí bản vá, các mô phỏng sẽ được chuyển vào hàm trang trí theo đúng thứ tự mà chúng đã áp dụng (thứ tự Python thông thường mà các trình trang trí được áp dụng). Điều này có nghĩa là từ dưới lên, vì vậy trong ví dụ trên, mô hình cho test_module.ClassName2 được chuyển vào đầu tiên.

Ngoài ra còn có patch.dict() để đặt giá trị trong từ điển chỉ trong phạm vi và khôi phục từ điển về trạng thái ban đầu khi quá trình kiểm tra kết thúc:

>>> foo = {'key': 'value'}
>>> original = foo.copy()
>>> with patch.dict(foo, {'newkey': 'newvalue'}, clear=True):
...     assert foo == {'newkey': 'newvalue'}
...
>>> assert foo == original

patch, patch.objectpatch.dict đều có thể được sử dụng làm trình quản lý bối cảnh.

Khi bạn sử dụng patch() để tạo một bản mô phỏng cho mình, bạn có thể lấy tham chiếu đến mô hình đó bằng cách sử dụng dạng "as" của câu lệnh with:

>>> class ProductionClass:
...     def method(self):
...         pass
...
>>> with patch.object(ProductionClass, 'method') as mock_method:
...     mock_method.return_value = None
...     real = ProductionClass()
...     real.method(1, 2, 3)
...
>>> mock_method.assert_called_with(1, 2, 3)

Là một patch thay thế, patch.objectpatch.dict có thể được sử dụng làm trang trí lớp. Khi được sử dụng theo cách này, nó cũng giống như việc áp dụng trình trang trí riêng lẻ cho mọi phương thức có tên bắt đầu bằng "test".

Các ví dụ khác

Dưới đây là một số ví dụ khác cho một số tình huống nâng cao hơn một chút.

Chế giễu các cuộc gọi bị xiềng xích

Việc mô phỏng các cuộc gọi chuỗi thực sự đơn giản với mô phỏng khi bạn hiểu thuộc tính return_value. Khi một bản mô phỏng được gọi lần đầu tiên hoặc bạn tìm nạp return_value của nó trước khi nó được gọi, một Mock mới sẽ được tạo.

Điều này có nghĩa là bạn có thể xem đối tượng được trả về từ lệnh gọi đến đối tượng bị mô phỏng đã được sử dụng như thế nào bằng cách thẩm vấn mô hình return_value:

>>> mock = Mock()
>>> mock().foo(a=2, b=3)
<Mock name='mock().foo()' id='...'>
>>> mock.return_value.foo.assert_called_with(a=2, b=3)

Từ đây, chỉ cần một bước đơn giản để định cấu hình và sau đó đưa ra xác nhận về các cuộc gọi theo chuỗi. Tất nhiên, một giải pháp thay thế khác là viết mã của bạn theo cách dễ kiểm tra hơn ngay từ đầu...

Vì vậy, giả sử chúng ta có một số mã trông hơi giống thế này:

>>> class Something:
...     def __init__(self):
...         self.backend = BackendProvider()
...     def method(self):
...         response = self.backend.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
...         # more code

Giả sử rằng BackendProvider đã được kiểm tra tốt, chúng tôi kiểm tra method() như thế nào? Cụ thể, chúng tôi muốn kiểm tra xem phần mã # more code có sử dụng đối tượng phản hồi đúng cách hay không.

Vì chuỗi lệnh gọi này được thực hiện từ một thuộc tính phiên bản nên chúng ta có thể vá thuộc tính backend trên phiên bản Something. Trong trường hợp cụ thể này, chúng tôi chỉ quan tâm đến giá trị trả về từ lệnh gọi cuối cùng tới start_call nên chúng tôi không có nhiều việc phải cấu hình. Giả sử đối tượng mà nó trả về là 'giống như tệp', vì vậy chúng tôi sẽ đảm bảo rằng đối tượng phản hồi của chúng tôi sử dụng open() dựng sẵn làm spec.

Để làm điều này, chúng tôi tạo một phiên bản mô phỏng làm chương trình phụ trợ mô phỏng và tạo một đối tượng phản hồi mô phỏng cho nó. Để đặt phản hồi làm giá trị trả về cho start_call cuối cùng đó, chúng ta có thể thực hiện điều này

mock_backend.get_endpoint.return_value.create_call.return_value.start_call.return_value = mock_response

Chúng ta có thể làm điều đó theo cách tốt hơn một chút bằng cách sử dụng phương thức configure_mock() để đặt trực tiếp giá trị trả về cho chúng ta:

>>> cái  đó = Cái  đó()
>>> mock_response =  phỏng(spec=open)
>>> mock_backend = Giả()
>>> config = {'get_endpoint.return_value.create_call.return_value.start_call.return_value': mock_response}
>>> mock_backend.configure_mock(**config)

Với những thứ này, chúng tôi sẽ vá "phụ trợ giả" tại chỗ và có thể thực hiện cuộc gọi thực sự

>>> something.backend = mock_backend
>>> something.method()

Sử dụng mock_calls, chúng ta có thể kiểm tra cuộc gọi theo chuỗi chỉ bằng một lần xác nhận. Cuộc gọi theo chuỗi là một số cuộc gọi trong một dòng mã, do đó sẽ có một số mục trong mock_calls. Chúng tôi có thể sử dụng call.call_list() để tạo danh sách cuộc gọi này cho chúng tôi

>>> chained = call.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
>>> call_list = chained.call_list()
>>> khẳng định mock_backend.mock_calls == call_list

Chế nhạo một phần

Đối với một số thử nghiệm, bạn có thể muốn thực hiện lệnh gọi tới datetime.date.today() để trả về một ngày đã biết nhưng không muốn ngăn mã đang được thử nghiệm tạo đối tượng ngày mới. Thật không may, datetime.date được viết bằng C, vì vậy bạn không thể chỉ vá lỗi phương thức datetime.date.today() tĩnh.

Thay vào đó, bạn có thể bao bọc lớp ngày bằng một bản mô phỏng một cách hiệu quả, đồng thời chuyển các lệnh gọi từ hàm tạo sang lớp thực (và trả về các phiên bản thực).

Ở đây, patch decorator được sử dụng để mô phỏng lớp date trong mô-đun đang được thử nghiệm. Sau đó, thuộc tính side_effect trên lớp ngày mô phỏng được đặt thành hàm lambda trả về ngày thực. Khi lớp ngày mô phỏng được gọi, ngày thực sẽ được tạo và trả về bởi side_effect.

>>> nhập ngày giờ dưới dạng dt
>>> với bản ('mymodule.date')  mock_date:
... mock_date.today.return_value = dt.date(2010, 10, 8)
... mock_date.side_effect = lambda *args, **kw: dt.date(*args, **kw)
...
... khẳng định mymodule.date.today() == dt.date(2010, 10, 8)
... khẳng định mymodule.date(2009, 6, 8) == dt.date(2009, 6, 8)

Lưu ý rằng chúng tôi không vá datetime.date trên toàn cầu, chúng tôi vá date trong mô-đun chứa uses đó. Xem where to patch.

Khi date.today() được gọi, ngày đã biết sẽ được trả về, nhưng các lệnh gọi tới hàm tạo date(...) vẫn trả về ngày bình thường. Nếu không có điều này, bạn có thể thấy mình phải tính toán kết quả mong đợi bằng cách sử dụng chính xác thuật toán giống như mã đang được thử nghiệm, đây là một mô hình chống thử nghiệm cổ điển.

Các cuộc gọi đến hàm tạo ngày được ghi lại trong thuộc tính mock_date (call_count và bạn bè), điều này cũng có thể hữu ích cho các thử nghiệm của bạn.

Một cách khác để xử lý ngày mô phỏng hoặc các lớp dựng sẵn khác được thảo luận trong this blog entry.

Chế nhạo một phương pháp tạo

Trình tạo Python là một hàm hoặc phương thức sử dụng câu lệnh yield để trả về một chuỗi giá trị khi lặp qua [1].

Một phương thức/hàm tạo được gọi để trả về đối tượng trình tạo. Sau đó, đối tượng trình tạo sẽ được lặp lại. Phương thức giao thức cho phép lặp là __iter__(), vì vậy chúng ta có thể mô phỏng điều này bằng cách sử dụng MagicMock.

Đây là một lớp ví dụ với phương thức "iter" được triển khai dưới dạng trình tạo:

>>> class Foo:
...     def iter(self):
...         for i in [1, 2, 3]:
...             yield i
...
>>> foo = Foo()
>>> list(foo.iter())
[1, 2, 3]

Chúng ta sẽ mô phỏng lớp này như thế nào và đặc biệt là phương thức "iter" của nó?

Để định cấu hình các giá trị được trả về từ lần lặp (ẩn trong lệnh gọi tới list), chúng ta cần định cấu hình đối tượng được trả về bởi lệnh gọi tới foo.iter().

>>> mock_foo = MagicMock()
>>> mock_foo.iter.return_value = iter([1, 2, 3])
>>> list(mock_foo.iter())
[1, 2, 3]

Áp dụng cùng một bản vá cho mọi phương pháp thử nghiệm

Nếu bạn muốn có nhiều bản vá cho nhiều phương pháp thử nghiệm thì cách rõ ràng là áp dụng các trình trang trí bản vá cho mọi phương pháp. Điều này có thể khiến bạn cảm thấy như sự lặp lại không cần thiết. Thay vào đó, bạn có thể sử dụng patch() (ở mọi dạng khác nhau) làm công cụ trang trí lớp. Điều này áp dụng các bản vá cho tất cả các phương pháp kiểm tra trên lớp. Phương thức thử nghiệm được xác định bằng các phương thức có tên bắt đầu bằng test:

>>> @patch('mymodule.SomeClass')
... lớp MyTest(unittest.TestCase):
...
... def test_one(self, MockSomeClass):
... self.assertIs(mymodule.SomeClass, MockSomeClass)
...
... def test_two(self, MockSomeClass):
... self.assertIs(mymodule.SomeClass, MockSomeClass)
...
... def not_a_test(self):
... trả lại 'thứ gì đó'
...
>>> MyTest('test_one').test_one()
>>> MyTest('test_two').test_two()
>>> MyTest('test_two').not_a_test()
'cái gì đó'

Một cách khác để quản lý các bản vá là sử dụng phương pháp vá: bắt đầu và dừng. Những điều này cho phép bạn di chuyển bản vá vào các phương thức setUptearDown của mình.

>>> lớp MyTest(unittest.TestCase):
... def setUp(self):
... self.patcher = patch('mymodule.foo')
... self.mock_foo = self.patcher.start()
...
... def test_foo(self):
... self.assertIs(mymodule.foo, self.mock_foo)
...
... def TearsDown(self):
... self.patcher.stop()
...
>>> MyTest('test_foo').run()

Nếu bạn sử dụng kỹ thuật này, bạn phải đảm bảo rằng việc vá lỗi được "hoàn tác" bằng cách gọi stop. Điều này có thể phức tạp hơn bạn nghĩ, bởi vì nếu một ngoại lệ được đưa ra trong setUp thì TearsDown sẽ không được gọi. unittest.TestCase.addCleanup() làm cho việc này trở nên dễ dàng hơn:

>>> lớp MyTest(unittest.TestCase):
... def setUp(self):
... patcher = patch('mymodule.foo')
... self.addCleanup(patcher.stop)
... self.mock_foo = patcher.start()
...
... def test_foo(self):
... self.assertIs(mymodule.foo, self.mock_foo)
...
>>> MyTest('test_foo').run()

Chế nhạo các phương pháp không ràng buộc

Đôi khi một bài kiểm tra cần vá một unbound method, có nghĩa là vá phương thức trên lớp thay vì trên phiên bản. Để đưa ra xác nhận về đối tượng nào đang gọi phương thức cụ thể này, bạn cần chuyển self làm đối số đầu tiên. Vấn đề là bạn không thể vá bằng một mô hình cho việc này, bởi vì nếu bạn thay thế một phương thức không liên kết bằng một mô hình thì nó sẽ không trở thành một phương thức bị ràng buộc khi được tìm nạp từ phiên bản và do đó nó không được truyền vào self. Cách giải quyết là vá phương thức không liên kết bằng một hàm thực. Trình trang trí patch() giúp việc vá các phương thức bằng mô hình trở nên đơn giản đến mức việc phải tạo một hàm thực sẽ trở thành một mối phiền toái.

Nếu bạn chuyển autospec=True sang bản vá thì nó sẽ thực hiện việc vá bằng đối tượng hàm real. Đối tượng hàm này có cùng chữ ký với đối tượng mà nó đang thay thế, nhưng ủy quyền cho một bản mô phỏng bên trong. Bạn vẫn nhận được bản mô phỏng được tạo tự động theo cách giống hệt như trước đây. Tuy nhiên, điều đó có nghĩa là nếu bạn sử dụng nó để vá một phương thức không liên kết trên một lớp thì hàm giả định sẽ được chuyển thành một phương thức liên kết nếu nó được tìm nạp từ một thể hiện. Nó sẽ có self được chuyển vào làm đối số đầu tiên, đó chính xác là những gì cần thiết:

>>> class Foo:
...   def foo(self):
...     pass
...
>>> with patch.object(Foo, 'foo', autospec=True) as mock_foo:
...   mock_foo.return_value = 'foo'
...   foo = Foo()
...   foo.foo()
...
'foo'
>>> mock_foo.assert_called_once_with(foo)

Nếu chúng ta không sử dụng autospec=True thì phương thức không liên kết sẽ được vá bằng một phiên bản Mock thay vào đó và không được gọi bằng self.

Kiểm tra nhiều cuộc gọi bằng mô hình

mock có một API tuyệt vời để đưa ra các xác nhận về cách sử dụng các đối tượng giả của bạn.

>>> mock = Mock()
>>> mock.foo_bar.return_value = None
>>> mock.foo_bar('baz', spam='eggs')
>>> mock.foo_bar.assert_called_with('baz', spam='eggs')

Nếu mô hình của bạn chỉ được gọi một lần, bạn có thể sử dụng phương thức assert_called_once_with() để xác nhận rằng call_count là một.

>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
>>> mock.foo_bar()
>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
Traceback (most recent call last):
    ...
AssertionError: Expected 'foo_bar' to be called once. Called 2 times.
Calls: [call('baz', spam='eggs'), call()].

Cả assert_called_withassert_called_once_with đều đưa ra khẳng định về lệnh gọi most recent. Nếu mô hình của bạn sắp được gọi nhiều lần và bạn muốn đưa ra xác nhận về all thì những cuộc gọi đó bạn có thể sử dụng call_args_list:

>>> mock = Mock(return_value=None)
>>> mock(1, 2, 3)
>>> mock(4, 5, 6)
>>> mock()
>>> mock.call_args_list
[call(1, 2, 3), call(4, 5, 6), call()]

Trình trợ giúp call giúp bạn dễ dàng đưa ra xác nhận về các lệnh gọi này. Bạn có thể xây dựng danh sách các cuộc gọi dự kiến ​​và so sánh với call_args_list. Điều này trông khá giống với bản repr của call_args_list:

>>> expected = [call(1, 2, 3), call(4, 5, 6), call()]
>>> mock.call_args_list == expected
True

Đối phó với các đối số có thể thay đổi

Một tình huống khác rất hiếm gặp nhưng có thể khiến bạn khó chịu, đó là khi mô hình giả của bạn được gọi với các đối số có thể thay đổi. call_argscall_args_list lưu trữ references vào các đối số. Nếu các đối số bị thay đổi bởi mã đang được kiểm tra thì bạn không còn có thể đưa ra xác nhận về giá trị khi mô hình được gọi.

Dưới đây là một số mã ví dụ cho thấy sự cố. Hãy tưởng tượng các hàm sau được định nghĩa trong 'mymodule':

def Frob(val):
    vượt qua

def grob(val):
    "Đầu tiên là Frob và sau đó là Clear val"
    đông lạnh(val)
    val.clear()

Khi chúng tôi cố gắng kiểm tra xem grob có gọi frob với đối số chính xác hay không, hãy xem điều gì sẽ xảy ra

>>> với bản ('mymodule.frob')  mock_frob:
... giá trị = {6}
... mymodule.grob(val)
...
>>> giá trị
đặt()
>>> mock_frob.assert_gọi_with({6})
Traceback (cuộc gọi gần đây nhất):
    ...
AssertionError: Dự kiến: (({6},, {})
Được gọi với: ((set(),), {})

Một khả năng là giả lập sao chép các đối số bạn truyền vào. Điều này sau đó có thể gây ra vấn đề nếu bạn thực hiện các xác nhận dựa vào danh tính đối tượng để đảm bảo sự bình đẳng.

Đây là một giải pháp sử dụng chức năng side_effect. Nếu bạn cung cấp hàm side_effect cho một bản mô phỏng thì side_effect sẽ được gọi với cùng các đối số như mô hình. Điều này cho chúng ta cơ hội sao chép các đối số và lưu trữ chúng để xác nhận sau này. Trong ví dụ này, tôi đang sử dụng mô hình another để lưu trữ các đối số để tôi có thể sử dụng các phương thức mô phỏng để thực hiện xác nhận. Một lần nữa, chức năng trợ giúp thiết lập điều này cho tôi.

>>> từ bản sao nhập bản sâu
>>> từ nhập unittest.mock Mock, patch, DEFAULT
>>> def copy_call_args(giả):
... new_mock =  phỏng()
... def side_effect(*args, **kwargs):
... args = deepcopy(args)
... kwargs = deepcopy(kwargs)
... new_mock(*args, **kwargs)
... trả lại DEFAULT
... mock.side_effect = side_effect
... trả lại new_mock
...
>>> với bản ('mymodule.frob')  mock_frob:
... new_mock = copy_call_args(mock_frob)
... giá trị = {6}
... mymodule.grob(val)
...
>>> new_mock.assert_gọi_with({6})
>>> new_mock.call_args
gọi({6})

copy_call_args được gọi với mô hình sẽ được gọi. Nó trả về một mô hình mới mà chúng ta thực hiện xác nhận trên đó. Hàm side_effect tạo một bản sao của các đối số và gọi new_mock của chúng ta bằng bản sao đó.

Ghi chú

Nếu mô hình của bạn chỉ được sử dụng một lần thì có cách dễ dàng hơn để kiểm tra các đối số tại thời điểm chúng được gọi. Bạn có thể chỉ cần thực hiện việc kiểm tra bên trong hàm side_effect.

>>> def side_effect(arg):
...     assert arg == {6}
...
>>> mock = Mock(side_effect=side_effect)
>>> mock({6})
>>> mock(set())
Traceback (most recent call last):
    ...
AssertionError

Một cách tiếp cận khác là tạo một lớp con của Mock hoặc MagicMock sao chép (sử dụng copy.deepcopy()) các đối số. Đây là một ví dụ thực hiện:

>>> from copy import deepcopy
>>> class CopyingMock(MagicMock):
...     def __call__(self, /, *args, **kwargs):
...         args = deepcopy(args)
...         kwargs = deepcopy(kwargs)
...         return super().__call__(*args, **kwargs)
...
>>> c = CopyingMock(return_value=None)
>>> arg = set()
>>> c(arg)
>>> arg.add(1)
>>> c.assert_called_with(set())
>>> c.assert_called_with(arg)
Traceback (most recent call last):
    ...
AssertionError: expected call not found.
Expected: mock({1})
Actual: mock(set())
>>> c.foo
<CopyingMock name='mock.foo' id='...'>

Khi bạn phân lớp Mock hoặc MagicMock, tất cả các thuộc tính được tạo động và return_value sẽ tự động sử dụng lớp con của bạn. Điều đó có nghĩa là tất cả con của CopyingMock cũng sẽ có loại CopyingMock.

Các bản vá lồng nhau

Sử dụng bản vá làm trình quản lý bối cảnh là điều tốt, nhưng nếu bạn thực hiện nhiều bản vá, bạn có thể kết thúc bằng các câu lệnh thụt ngày càng xa về bên phải:

>>> lớp MyTest(unittest.TestCase):
...
... def test_foo(self):
... với bản ('mymodule.Foo')  mock_foo:
... với bản  ('mymodule.Bar')  mock_bar:
... với bản ('mymodule.Spam')  mock_spam:
... khẳng định mymodule.Foo  mock_foo
... khẳng định mymodule.Bar  mock_bar
... khẳng định mymodule.Spam  mock_spam
...
>>> gốc = mymodule.Foo
>>> MyTest('test_foo').test_foo()
>>> khẳng định mymodule.Foo  bản gốc

Với các hàm cleanup nhỏ nhất và phương pháp vá: bắt đầu và dừng, chúng ta có thể đạt được hiệu ứng tương tự mà không cần thụt lề lồng nhau. Một phương thức trợ giúp đơn giản, create_patch, đặt bản vá vào đúng vị trí và trả về bản mô phỏng đã tạo cho chúng ta:

>>> lớp MyTest(unittest.TestCase):
...
... def create_patch(tự, tên):
... patcher = patch(name)
... điều = patcher.start()
... self.addCleanup(patcher.stop)
... trả lại đồ
...
... def test_foo(self):
... mock_foo = self.create_patch('mymodule.Foo')
... mock_bar = self.create_patch('mymodule.Bar')
... mock_spam = self.create_patch('mymodule.Spam')
...
... khẳng định mymodule.Foo  mock_foo
... khẳng định mymodule.Bar  mock_bar
... khẳng định mymodule.Spam  mock_spam
...
>>> gốc = mymodule.Foo
>>> MyTest('test_foo').run()
>>> khẳng định mymodule.Foo  bản gốc

Chế nhạo từ điển bằng MagicMock

Bạn có thể muốn mô phỏng một từ điển hoặc đối tượng chứa khác, ghi lại tất cả quyền truy cập vào nó trong khi nó vẫn hoạt động giống như một từ điển.

Chúng tôi có thể thực hiện việc này với MagicMock, nó sẽ hoạt động giống như một từ điển và sử dụng side_effect để ủy quyền quyền truy cập từ điển vào một từ điển cơ bản thực sự nằm dưới sự kiểm soát của chúng tôi.

Khi các phương thức __getitem__()__setitem__() của MagicMock của chúng tôi được gọi (truy cập từ điển thông thường) thì side_effect được gọi bằng khóa (và trong trường hợp __setitem__ thì cả giá trị nữa). Chúng tôi cũng có thể kiểm soát những gì được trả lại.

Sau khi sử dụng MagicMock, chúng ta có thể sử dụng các thuộc tính như call_args_list để xác nhận về cách sử dụng từ điển:

>>> my_dict = {'a': 1, 'b': 2, 'c': 3}
>>> def getitem(name):
...      return my_dict[name]
...
>>> def setitem(name, val):
...     my_dict[name] = val
...
>>> mock = MagicMock()
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem

Ghi chú

Một cách khác để sử dụng MagicMock là sử dụng Mockonly cung cấp các phương pháp kỳ diệu mà bạn đặc biệt muốn:

>>> mock = Mock()
>>> mock.__getitem__ = Mock(side_effect=getitem)
>>> mock.__setitem__ = Mock(side_effect=setitem)

Tùy chọn third là sử dụng MagicMock nhưng chuyển vào dict làm đối số spec (hoặc spec_set) để MagicMock được tạo chỉ có sẵn các phương thức ma thuật từ điển:

>>> mock = MagicMock(spec_set=dict)
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem

Với các chức năng tác dụng phụ này, mock sẽ hoạt động giống như một từ điển bình thường nhưng ghi lại quyền truy cập. Nó thậm chí còn tăng KeyError nếu bạn cố truy cập vào một khóa không tồn tại.

>>> mock['a']
1
>>> mock['c']
3
>>> mock['d']
Traceback (most recent call last):
    ...
KeyError: 'd'
>>> mock['b'] = 'fish'
>>> mock['d'] = 'eggs'
>>> mock['b']
'fish'
>>> mock['d']
'eggs'

Sau khi nó được sử dụng, bạn có thể đưa ra các xác nhận về quyền truy cập bằng các thuộc tính và phương thức mô phỏng thông thường:

>>> mock.__getitem__.call_args_list
[call('a'), call('c'), call('d'), call('b'), call('d')]
>>> mock.__setitem__.call_args_list
[call('b', 'fish'), call('d', 'eggs')]
>>> my_dict
{'a': 1, 'b': 'fish', 'c': 3, 'd': 'eggs'}

Các lớp con mô phỏng và thuộc tính của chúng

Có nhiều lý do khác nhau khiến bạn muốn phân lớp Mock. Một lý do có thể là thêm các phương thức trợ giúp. Đây là một ví dụ ngớ ngẩn:

>>> class MyMock(MagicMock):
...     def has_been_called(self):
...         return self.called
...
>>> mymock = MyMock(return_value=None)
>>> mymock
<MyMock id='...'>
>>> mymock.has_been_called()
False
>>> mymock()
>>> mymock.has_been_called()
True

Hành vi tiêu chuẩn cho các phiên bản Mock là các thuộc tính và mô hình giá trị trả về có cùng loại với mô hình mà chúng được truy cập. Điều này đảm bảo rằng thuộc tính MockMocks và thuộc tính MagicMockMagicMocks [2]. Vì vậy, nếu bạn đang phân lớp để thêm các phương thức trợ giúp thì chúng cũng sẽ có sẵn trên các thuộc tính và giá trị trả về mô phỏng các phiên bản của lớp con của bạn.

>>> mymock.foo
<MyMock name='mock.foo' id='...'>
>>> mymock.foo.has_been_called()
False
>>> mymock.foo()
<MyMock name='mock.foo()' id='...'>
>>> mymock.foo.has_been_called()
True

Đôi khi điều này thật bất tiện. Ví dụ: one user là mô phỏng phân lớp con để tạo Twisted adaptor. Việc áp dụng điều này cho các thuộc tính cũng thực sự gây ra lỗi.

Mock (trong tất cả các phiên bản của nó) sử dụng một phương thức có tên _get_child_mock để tạo các "mô hình phụ" này cho các thuộc tính và giá trị trả về. Bạn có thể ngăn việc sử dụng lớp con của mình cho các thuộc tính bằng cách ghi đè phương thức này. Đặc điểm là nó lấy các đối số từ khóa tùy ý (**kwargs) sau đó được chuyển vào hàm tạo giả:

>>> class Subclass(MagicMock):
...     def _get_child_mock(self, /, **kwargs):
...         return MagicMock(**kwargs)
...
>>> mymock = Subclass()
>>> mymock.foo
<MagicMock name='mock.foo' id='...'>
>>> assert isinstance(mymock, Subclass)
>>> assert not isinstance(mymock.foo, Subclass)
>>> assert not isinstance(mymock(), Subclass)

Chế nhạo nhập khẩu bằng patch.dict

Một tình huống mà việc chế nhạo có thể khó khăn là khi bạn nhập cục bộ bên trong một hàm. Những điều này khó mô phỏng hơn vì chúng không sử dụng một đối tượng từ không gian tên mô-đun mà chúng ta có thể vá lỗi.

Nói chung nên tránh nhập khẩu nội địa. Đôi khi chúng được thực hiện để ngăn chặn sự phụ thuộc vòng tròn, trong đó usually có một cách tốt hơn nhiều để giải quyết vấn đề (cấu trúc lại mã) hoặc để ngăn chặn "chi phí trả trước" bằng cách trì hoãn việc nhập. Điều này cũng có thể được giải quyết theo những cách tốt hơn so với nhập cục bộ vô điều kiện (lưu trữ mô-đun dưới dạng thuộc tính lớp hoặc mô-đun và chỉ thực hiện nhập khi sử dụng lần đầu).

Ngoài ra, còn có một cách sử dụng mock để ảnh hưởng đến kết quả nhập. Quá trình nhập sẽ tìm nạp object từ từ điển sys.modules. Lưu ý rằng nó tìm nạp object, không cần phải là mô-đun. Việc nhập mô-đun lần đầu tiên dẫn đến đối tượng mô-đun được đặt trong sys.modules, do đó, thông thường khi bạn nhập nội dung nào đó, bạn sẽ nhận được mô-đun trở lại. Tuy nhiên, điều này không cần thiết phải như vậy.

Điều này có nghĩa là bạn có thể sử dụng patch.dict() để temporarily đặt một bản mô phỏng vào sys.modules. Mọi hoạt động nhập trong khi bản vá này đang hoạt động sẽ lấy bản mô phỏng. Khi bản vá hoàn tất (hàm trang trí thoát, phần thân câu lệnh with hoàn tất hoặc patcher.stop() được gọi) thì mọi thứ ở đó trước đó sẽ được khôi phục an toàn.

Đây là một ví dụ mô phỏng mô-đun 'ngu ngốc'.

>>> import sys
>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
...    import fooble
...    fooble.blob()
...
<Mock name='mock.blob()' id='...'>
>>> assert 'fooble' not in sys.modules
>>> mock.blob.assert_called_once_with()

Như bạn có thể thấy import fooble thành công, nhưng khi thoát ra, không còn 'kẻ ngốc' nào trong sys.modules.

Điều này cũng hoạt động với dạng from module import name:

>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
...    from fooble import blob
...    blob.blip()
...
<Mock name='mock.blob.blip()' id='...'>
>>> mock.blob.blip.assert_called_once_with()

Với công việc nhiều hơn một chút, bạn cũng có thể mô phỏng việc nhập gói:

>>> mock = Mock()
>>> modules = {'package': mock, 'package.module': mock.module}
>>> with patch.dict('sys.modules', modules):
...    from package.module import fooble
...    fooble()
...
<Mock name='mock.module.fooble()' id='...'>
>>> mock.module.fooble.assert_called_once_with()

Theo dõi thứ tự cuộc gọi và xác nhận cuộc gọi ít chi tiết hơn

Lớp Mock cho phép bạn theo dõi order của các lệnh gọi phương thức trên các đối tượng mô phỏng của bạn thông qua thuộc tính method_calls. Điều này không cho phép bạn theo dõi thứ tự cuộc gọi giữa các đối tượng mô phỏng riêng biệt, tuy nhiên chúng ta có thể sử dụng mock_calls để đạt được hiệu quả tương tự.

Bởi vì các mô phỏng theo dõi các cuộc gọi đến các mô phỏng con trong mock_calls và việc truy cập vào một thuộc tính tùy ý của một mô phỏng sẽ tạo ra một mô phỏng con, nên chúng ta có thể tạo các mô phỏng riêng biệt từ mô phỏng gốc. Sau đó, tất cả các cuộc gọi đến mô phỏng con đó sẽ được ghi lại theo thứ tự trong mock_calls của cha mẹ:

>>> manager = Mock()
>>> mock_foo = manager.foo
>>> mock_bar = manager.bar
>>> mock_foo.something()
<Mock name='mock.foo.something()' id='...'>
>>> mock_bar.other.thing()
<Mock name='mock.bar.other.thing()' id='...'>
>>> manager.mock_calls
[call.foo.something(), call.bar.other.thing()]

Sau đó, chúng tôi có thể xác nhận về các lệnh gọi, bao gồm cả thứ tự, bằng cách so sánh với thuộc tính mock_calls trên mô hình trình quản lý:

>>> expected_calls = [call.foo.something(), call.bar.other.thing()]
>>> manager.mock_calls == expected_calls
True

Nếu patch đang tạo và đặt các mô hình của bạn vào vị trí thì bạn có thể đính kèm chúng vào một mô hình quản lý bằng phương pháp attach_mock(). Sau khi đính kèm cuộc gọi sẽ được ghi vào mock_calls của người quản lý.

>>> người quản  = MagicMock()
>>> với bản ('mymodule.Class1')  MockClass1:
... với bản ('mymodule.Class2')  MockClass2:
... manager.attach_mock(MockClass1, 'MockClass1')
... manager.attach_mock(MockClass2, 'MockClass2')
... MockClass1().foo()
... MockClass2().bar()
<MagicMock name='mock.MockClass1().foo()' id='...'>
<MagicMock name='mock.MockClass2().bar()' id='...'>
>>> manager.mock_calls
[gọi.MockClass1(),
gọi.MockClass1().foo(),
gọi.MockClass2(),
call.MockClass2().bar()]

Nếu nhiều cuộc gọi đã được thực hiện nhưng bạn chỉ quan tâm đến một chuỗi cụ thể trong số đó thì một giải pháp thay thế là sử dụng phương pháp assert_has_calls(). Cái này lấy danh sách các cuộc gọi (được xây dựng bằng đối tượng call). Nếu chuỗi lệnh gọi đó nằm trong mock_calls thì xác nhận thành công.

>>> m = MagicMock()
>>> m().foo().bar().baz()
<MagicMock name='mock().foo().bar().baz()' id='...'>
>>> m.one().two().three()
<MagicMock name='mock.one().two().three()' id='...'>
>>> calls = call.one().two().three().call_list()
>>> m.assert_has_calls(calls)

Mặc dù lệnh gọi m.one().two().three() theo chuỗi không phải là lệnh gọi duy nhất được thực hiện cho bản mô phỏng, nhưng khẳng định vẫn thành công.

Đôi khi một mô phỏng có thể có một số lệnh gọi được thực hiện và bạn chỉ quan tâm đến việc xác nhận khoảng some của các lệnh gọi đó. Bạn thậm chí có thể không quan tâm đến thứ tự. Trong trường hợp này bạn có thể chuyển any_order=True sang assert_has_calls:

>>> m = MagicMock()
>>> m(1), m.two(2, 3), m.seven(7), m.fifty('50')
(...)
>>> calls = [call.fifty('50'), call(1), call.seven(7)]
>>> m.assert_has_calls(calls, any_order=True)

Kết hợp đối số phức tạp hơn

Sử dụng khái niệm cơ bản tương tự như ANY, chúng ta có thể triển khai trình so khớp để thực hiện các xác nhận phức tạp hơn trên các đối tượng được sử dụng làm đối số cho mô phỏng.

Giả sử chúng ta mong đợi một số đối tượng được chuyển đến một mô hình giả mà theo mặc định sẽ so sánh bằng nhau dựa trên danh tính đối tượng (là mặc định của Python cho các lớp do người dùng xác định). Để sử dụng assert_called_with(), chúng ta cần truyền vào cùng một đối tượng. Nếu chúng ta chỉ quan tâm đến một số thuộc tính của đối tượng này thì chúng ta có thể tạo một trình so khớp để kiểm tra các thuộc tính này cho chúng ta.

Bạn có thể thấy trong ví dụ này lệnh gọi 'tiêu chuẩn' tới assert_called_with là không đủ:

>>> class Foo:
...     def __init__(self, a, b):
...         self.a, self.b = a, b
...
>>> mock = Mock(return_value=None)
>>> mock(Foo(1, 2))
>>> mock.assert_called_with(Foo(1, 2))
Traceback (most recent call last):
    ...
AssertionError: expected call not found.
Expected: mock(<__main__.Foo object at 0x...>)
Actual: mock(<__main__.Foo object at 0x...>)

Hàm so sánh cho lớp Foo của chúng ta có thể trông giống như thế này:

>>> def compare(self, other):
...     if not type(self) == type(other):
...         return False
...     if self.a != other.a:
...         return False
...     if self.b != other.b:
...         return False
...     return True
...

Và một đối tượng so khớp có thể sử dụng các hàm so sánh như thế này cho phép tính đẳng thức của nó sẽ trông giống như thế này:

>>> class Matcher:
...     def __init__(self, compare, some_obj):
...         self.compare = compare
...         self.some_obj = some_obj
...     def __eq__(self, other):
...         return self.compare(self.some_obj, other)
...

Đặt tất cả những thứ này lại với nhau:

>>> match_foo = Matcher(compare, Foo(1, 2))
>>> mock.assert_called_with(match_foo)

Matcher được khởi tạo bằng hàm so sánh của chúng tôi và đối tượng Foo mà chúng tôi muốn so sánh. Trong assert_called_with, phương thức đẳng thức Matcher sẽ được gọi, phương thức này sẽ so sánh đối tượng mà mô hình được gọi với đối tượng mà chúng ta đã tạo đối tượng so khớp. Nếu chúng khớp nhau thì assert_called_with sẽ vượt qua và nếu không thì AssertionError sẽ được nâng lên:

>>> match_wrong = Matcher(compare, Foo(3, 4))
>>> mock.assert_called_with(match_wrong)
Traceback (most recent call last):
    ...
AssertionError: Expected: ((<Matcher object at 0x...>,), {})
Called with: ((<Foo object at 0x...>,), {})

Với một chút tinh chỉnh, bạn có thể yêu cầu chức năng so sánh nâng trực tiếp AssertionError và cung cấp thông báo lỗi hữu ích hơn.

Kể từ phiên bản 1.5, thư viện thử nghiệm Python PyHamcrest cung cấp chức năng tương tự, có thể hữu ích ở đây, dưới dạng trình so khớp đẳng thức (hamcrest.library.integration.match_equality).