16

Tôi đang làm việc trên một dự án liên quan đến kết nối với máy chủ từ xa, chờ phản hồi và sau đó thực hiện các hành động dựa trên phản hồi đó. Chúng tôi bắt một số ngoại lệ khác nhau và hoạt động khác nhau tùy thuộc vào ngoại lệ bị bắt. Ví dụ:Không thể bắt ngoại lệ giả mạo vì nó không thừa hưởng BaseException

def myMethod(address, timeout=20): 
    try: 
     response = requests.head(address, timeout=timeout) 
    except requests.exceptions.Timeout: 
     # do something special 
    except requests.exceptions.ConnectionError: 
     # do something special 
    except requests.exceptions.HTTPError: 
     # do something special 
    else: 
     if response.status_code != requests.codes.ok: 
      # do something special 
     return successfulConnection.SUCCESS 

Để kiểm tra điều này, chúng tôi đã viết một bài kiểm tra như sau

class TestMyMethod(unittest.TestCase): 

    def test_good_connection(self): 
     config = { 
      'head.return_value': type('MockResponse',(), {'status_code': requests.codes.ok}), 
      'codes.ok': requests.codes.ok 
     } 
     with mock.patch('path.to.my.package.requests', **config): 
      self.assertEqual(
       mypackage.myMethod('some_address', 
       mypackage.successfulConnection.SUCCESS 
      ) 

    def test_bad_connection(self): 
     config = { 
      'head.side_effect': requests.exceptions.ConnectionError, 
      'requests.exceptions.ConnectionError': requests.exceptions.ConnectionError 
     } 
     with mock.patch('path.to.my.package.requests', **config): 
      self.assertEqual(
       mypackage.myMethod('some_address', 
       mypackage.successfulConnection.FAILURE 
      ) 

Nếu tôi chạy chức năng trực tiếp, tất cả mọi thứ xảy ra như mong đợi. Tôi thậm chí đã thử nghiệm bằng cách thêm raise requests.exceptions.ConnectionError vào mệnh đề try của hàm. Nhưng khi tôi chạy thử nghiệm đơn vị của tôi, tôi nhận được

ERROR: test_bad_connection (test.test_file.TestMyMethod) 
---------------------------------------------------------------- 
Traceback (most recent call last): 
    File "path/to/sourcefile", line ###, in myMethod 
    respone = requests.head(address, timeout=timeout) 
    File "path/to/unittest/mock", line 846, in __call__ 
    return _mock_self.mock_call(*args, **kwargs) 
    File "path/to/unittest/mock", line 901, in _mock_call 
    raise effect 
my.package.requests.exceptions.ConnectionError 

During handling of the above exception, another exception occurred: 

Traceback (most recent call last): 
    File "Path/to/my/test", line ##, in test_bad_connection 
    mypackage.myMethod('some_address', 
    File "Path/to/package", line ##, in myMethod 
    except requests.exceptions.ConnectionError: 
TypeError: catching classes that do not inherit from BaseException is not allowed 

Tôi cố gắng để thay đổi ngoại trừ tôi đã vá để BaseException và tôi đã nhận một lỗi nhiều hay ít giống hệt nhau.

Tôi đã đọc https://stackoverflow.com/a/18163759/3076272 rồi, vì vậy tôi nghĩ rằng nó phải là một móc xấu __del__ ở đâu đó, nhưng tôi không chắc chắn nơi để tìm nó hoặc những gì tôi thậm chí có thể làm trong thời gian có nghĩa. Tôi cũng tương đối mới với unittest.mock.patch() vì vậy rất có thể tôi đang làm điều gì đó sai ở đó.

Đây là một bổ trợ Fusion360 vì vậy nó đang sử dụng phiên bản đóng gói của Fusion 360 của Python 3.3 - theo như tôi biết đó là phiên bản vani (nghĩa là chúng không tự cuộn) nhưng tôi không tích cực .

+0

FWIW Tôi đã có thể giải quyết vấn đề này bằng cách sử dụng http://stackoverflow.com/a/28507806/3076272, nhưng tôi muốn tìm hiểu lý do tại sao điều này xảy ra. – Dannnno

+0

Có phải 'yêu cầu' ở đây là mô-đun của riêng bạn hay là mô-đun' yêu cầu' từ http://www.python-requests.org/? –

+0

Một từ python-requests.org – Dannnno

Trả lời

18

tôi có thể sao chép các lỗi với một ví dụ nhỏ:

foo.py:

class MyError(Exception): 
    pass 

class A: 
    def inner(self): 
     err = MyError("FOO") 
     print(type(err)) 
     raise err 
    def outer(self): 
     try: 
      self.inner() 
     except MyError as err: 
      print ("catched ", err) 
     return "OK" 

thử nghiệm mà không chế giễu:

class FooTest(unittest.TestCase): 
    def test_inner(self): 
     a = foo.A() 
     self.assertRaises(foo.MyError, a.inner) 
    def test_outer(self): 
     a = foo.A() 
     self.assertEquals("OK", a.outer()) 

Ok, tất cả là tốt, cả hai thử nghiệm vượt qua

Sự cố xảy ra với mocks. Ngay khi MyError lớp được chế giễu, mệnh đề expect không thể bắt bất cứ điều gì và tôi nhận được cùng một lỗi như ví dụ từ câu hỏi:

class FooTest(unittest.TestCase): 
    def test_inner(self): 
     a = foo.A() 
     self.assertRaises(foo.MyError, a.inner) 
    def test_outer(self): 
     with unittest.mock.patch('foo.MyError'): 
      a = exc2.A() 
      self.assertEquals("OK", a.outer()) 

Ngay lập tức cung cấp cho:

ERROR: test_outer (__main__.FooTest) 
---------------------------------------------------------------------- 
Traceback (most recent call last): 
    File "...\foo.py", line 11, in outer 
    self.inner() 
    File "...\foo.py", line 8, in inner 
    raise err 
TypeError: exceptions must derive from BaseException 

During handling of the above exception, another exception occurred: 

Traceback (most recent call last): 
    File "<pyshell#78>", line 8, in test_outer 
    File "...\foo.py", line 12, in outer 
    except MyError as err: 
TypeError: catching classes that do not inherit from BaseException is not allowed 

Ở đây tôi nhận được một TypeError đầu tiên mà bạn không có, bởi vì tôi đang nuôi một mô hình trong khi bạn buộc một ngoại lệ thực sự với cấu hình 'requests.exceptions.ConnectionError': requests.exceptions.ConnectionError. Nhưng vấn đề vẫn còn là mệnh đề except cố gắng bắt một mô hình.

TL/DR: khi bạn giả lập gói đầy đủ requests, mệnh đề except requests.exceptions.ConnectionError sẽ cố bắt một mô hình. Vì mô hình không thực sự là một BaseException, nó gây ra lỗi.

Giải pháp duy nhất tôi có thể tưởng tượng là không giả lập đầy đủ requests nhưng chỉ các phần không ngoại lệ. Tôi phải thừa nhận rằng tôi không thể tìm cách nói giả lập giả lập mọi thứ ngoại trừ nhưng trong ví dụ của bạn, bạn chỉ cần vá requests.head.Vì vậy, tôi nghĩ rằng điều này sẽ làm việc:

def test_bad_connection(self): 
    with mock.patch('path.to.my.package.requests.head', 
        side_effect=requests.exceptions.ConnectionError): 
     self.assertEqual(
      mypackage.myMethod('some_address', 
      mypackage.successfulConnection.FAILURE 
     ) 

Đó là: chỉ vá các phương pháp head với ngoại lệ là tác dụng phụ.

+1

câu trả lời chi tiết tuyệt vời! +1! – AlejandroVK

+0

Câu trả lời tuyệt vời, ước gì tôi có thể cung cấp thêm nhiều upvotes. – wesanyer

3

Tôi vừa gặp sự cố tương tự khi cố gắng giả lập sqlite3 (và tìm thấy bài đăng này trong khi tìm kiếm giải pháp).

Serge nói là đúng:

TL/DR: as you mock the full requests package, the except requests.exceptions.ConnectionError clause tries to catch a mock. As the mock is not really a BaseException, it causes the error.

The only solution I can imagine is not to mock the full requests but only the parts that are not exceptions. I must admit I could not find how to say to mock mock everything except this

Giải pháp của tôi là để thử toàn bộ mô-đun, sau đó thiết lập các thuộc tính giả đối với ngoại lệ là bằng với ngoại lệ trong lớp thực, hiệu quả "un-mocking " sự ngoại lệ. Ví dụ, trong trường hợp của tôi:

@mock.patch(MyClass.sqlite3) 
def test_connect_fail(self, mock_sqlite3): 
    mock_sqlite3.connect.side_effect = sqlite3.OperationalError() 
    mock_sqlite3.OperationalError = sqlite3.OperationalError 
    self.assertRaises(sqlite3.OperationalError, MyClass, self.db_filename) 

cho requests, bạn có thể gán ngoại lệ riêng như thế này:

mock_requests.exceptions.ConnectionError = requests.exceptions.ConnectionError 

hoặc làm điều đó cho tất cả các requests trường hợp ngoại lệ như thế này:

mock_requests.exceptions = requests.exceptions 

Tôi không biết nếu đây là cách "đúng" để làm điều đó, nhưng cho đến nay nó có vẻ làm việc cho tôi mà không có bất kỳ vấn đề.

1

Đối với những người trong chúng ta, những người cần để chế nhạo một ngoại lệ và không thể làm điều đó bằng cách đơn giản vá head, đây là một giải pháp dễ dàng thay thế ngoại trừ mục tiêu với một trống một:

Giả sử chúng ta có một đơn vị chung thử nghiệm với một ngoại lệ, chúng ta phải có chế giễu:

# app/foo_file.py 
def test_me(): 
    try: 
     foo() 
     return "No foo error happened" 
    except CustomError: # <-- Mock me! 
     return "The foo error was caught" 

chúng tôi muốn thử CustomError nhưng bởi vì nó là một ngoại lệ, chúng tôi chạy vào rắc rối nếu chúng ta cố gắng vá nó giống như mọi thứ khác. Thông thường, một cuộc gọi đến patch sẽ thay thế mục tiêu bằng MagicMock nhưng điều đó sẽ không hoạt động ở đây. Mocks rất tiện lợi, nhưng chúng không hoạt động như ngoại lệ. Thay vì vá bằng một mô hình, thay vào đó, hãy cho nó một ngoại lệ. Chúng tôi sẽ làm điều đó trong tệp thử nghiệm của chúng tôi.

# app/test_foo_file.py 
from mock import patch 


# A do-nothing exception we are going to replace CustomError with 
class StubException(Exception): 
    pass 


# Now apply it to our test 
@patch('app.foo_file.foo') 
@patch('app.foo_file.CustomError', new_callable=lambda: StubException) 
def test_foo(stub_exception, mock_foo): 
    mock_foo.side_effect = stub_exception("Stub") # Raise our stub to be caught by CustomError 
    assert test_me() == "The error was caught" 

# Success! 

Vậy điều gì với lambda? new_callable param gọi bất cứ điều gì chúng tôi cung cấp cho nó và thay thế các mục tiêu với sự trở lại của cuộc gọi đó. Nếu chúng ta vượt qua StubException lớp của chúng tôi thẳng, nó sẽ gọi constructor của lớp và vá đối tượng mục tiêu của chúng tôi với một ví dụ ngoại lệ chứ không phải là một lớp mà không phải là những gì chúng ta muốn. Bằng cách gói nó với lambda, nó sẽ trả về lớp của chúng tôi như chúng tôi dự định.

Khi bản vá của chúng tôi được thực hiện, đối tượng stub_exception (nghĩa là lớp StubException) của chúng tôi có thể được nâng lên và bị bắt như thể đó là CustomError. Khéo léo!

+0

Ngoài sự tò mò, tại sao bạn lại cần phải giả lập một ngoại lệ? – Dannnno

+0

@Dannnno: Câu hỏi hay. Chúng tôi giả lập một ngoại lệ cho cùng một lý do chúng tôi giả lập bất cứ điều gì - để ngăn chặn logic của nó chạy trong thử nghiệm đơn vị của chúng tôi. Thông thường, các ngoại lệ chỉ thừa hưởng lớp ngoại lệ cơ bản và không làm gì khác. Trong trường hợp này chế nhạo chúng là vô nghĩa bởi vì ngoại lệ chúng ta rút ra và ngoại lệ chúng ta thay thế nó bằng lôgic giống hệt nhau, nhưng mỗi bây giờ và sau đó ai đó xây dựng một ngoại lệ có logic và trong trường hợp hiếm hoi đó là điều tốt đẹp để có thể để chế giễu nó. – user2859458

+0

Tôi chỉ có một thời gian thực sự khó khăn tưởng tượng một tình huống mà một ngoại lệ nên ông làm bất kỳ logic đó là giá trị chế nhạo. Nếu có thể bạn có thể mở rộng về những gì đang xảy ra? Tôi đang thực sự làm việc trên một bản vá cho vấn đề này và nếu tôi đã có thể đưa ra một lý do chính đáng cho nó sẽ giúp ích rất nhiều. – Dannnno

0

Tôi gặp phải sự cố tương tự khi cố gắng thử gói sh. Trong khi sh là rất hữu ích, thực tế là tất cả các phương pháp và ngoại lệ được xác định tự động làm cho nó khó khăn hơn để thử chúng.Vì vậy, sau sự giới thiệu của documentation:

import unittest 
from unittest.mock import Mock, patch 


class MockSh(Mock): 
    # error codes are defined dynamically in sh 
    class ErrorReturnCode_32(BaseException): 
     pass 

    # could be any sh command  
    def mount(self, *args): 
     raise self.ErrorReturnCode_32 


class MyTestCase(unittest.TestCase): 
    mock_sh = MockSh() 

    @patch('core.mount.sh', new=mock_sh) 
    def test_mount(self): 
     ... 
0

Tôi chỉ chạy vào cùng một vấn đề khi chế giễu struct.

tôi nhận được lỗi:

TypeError: catching classes that do not inherit from BaseException is not allowed

Khi cố gắng để bắt một struct.error huy động từ struct.unpack.

Tôi thấy rằng cách đơn giản nhất để giải quyết vấn đề này trong thử nghiệm của tôi là chỉ cần đặt giá trị của thuộc tính lỗi trong mô hình của tôi là Exception. Ví dụ

Phương pháp Tôi muốn thử nghiệm có mô hình cơ bản này:

def some_meth(self): 
    try: 
     struct.unpack(fmt, data) 
    except struct.error: 
     return False 
    return True 

Xét nghiệm này có mô hình cơ bản này.

@mock.patch('my_module.struct') 
def test_some_meth(self, struct_mock): 
    '''Explain how some_func should work.''' 
    struct_mock.error = Exception 
    self.my_object.some_meth() 
    struct_mock.unpack.assert_called() 
    struct_mock.unpack.side_effect = struct_mock.error 
    self.assertFalse(self.my_object.some_meth() 

Điều này tương tự như cách tiếp cận được thực hiện bởi @BillB, nhưng nó đơn giản hơn vì tôi không cần thêm nhập vào thử nghiệm và vẫn nhận được hành vi tương tự. Đối với tôi, có vẻ như đây là kết luận hợp lý cho chủ đề chung của lý luận trong các câu trả lời ở đây.

Các vấn đề liên quan