【问题标题】:Python: block network connections for testing purposes?Python:为了测试目的而阻止网络连接?
【发布时间】:2013-09-03 21:31:08
【问题描述】:

我正在尝试测试一个为一些 Web 服务提供接口的包。它有一个测试套件,可以在连接到互联网的情况下测试大多数功能。但是,有一些挥之不去的测试可能会尝试连接到互联网/下载数据,我想阻止它们这样做有两个原因:首先,确保我的测试套件在没有可用网络连接的情况下工作;其次,这样我就不会使用过多的查询向 Web 服务发送垃圾邮件。

一个明显的解决方案是拔掉我的机器/关闭无线,但是当我在远程机器上运行测试时显然无法正常工作。

所以,我的问题是:我可以阻止单个 python 进程的网络/端口访问吗? (“沙盒”它,但只是阻止网络连接)

(afaict,pysandbox 不这样做)

编辑:我正在使用py.test,因此我需要一个可以与py.test 配合使用的解决方案,以防影响任何建议的答案。

【问题讨论】:

  • 对于那些可能正在寻找可定制的阻止和/或记录正在建立的连接的人,您最好使用vcrpy。有一个pytest plugin

标签: python pytest sandbox


【解决方案1】:

猴子补丁socket应该这样做:

import socket
def guard(*args, **kwargs):
    raise Exception("I told you not to use the Internet!")
socket.socket = guard

确保它在任何其他导入之前运行。

【讨论】:

  • 这太棒了!关于如何让 py.test 先运行它有什么想法吗?
  • 回答我的最后一条评论:在conftests.py 中运行。
  • 现在有一个 Py.test 插件,所以尽可能使用它。如果没有,您也许可以在socket 方法上使用patch 并将side_effect=Exception 作为参数传递。
  • 这是一个很好的解决方案,但值得注意的是它只影响使用 Python 套接字 API 的代码。直接调用系统的代码,例如。包装 C 库的模块不受此影响。
  • 这和以下任何解决方案都不适用于许多情况。甚至似乎都没有停止请求库。
【解决方案2】:

更新:现在有一个 pytest 插件可以做和这个答案一样的事情!您可以阅读答案以了解事情的运作方式,但我强烈建议使用插件而不是复制粘贴我的答案 :-) 请参阅此处:https://github.com/miketheman/pytest-socket


我发现 Thomas Orozco 的回答非常有帮助。继 keflavich 之后,这就是我集成到我的单元测试套件中的方式。这对我来说适用于数千个非常不同的单元测试用例(

我发布了here。为方便起见,包括以下内容。使用 Python 2.7.5 测试,pytest==2.7.0。 (要自己测试,请在克隆了所有 3 个文件的目录中运行 py.test --doctest-modules。)

_socket_toggle.py

from __future__ import print_function
import socket
import sys

_module = sys.modules[__name__]

def disable_socket():
    """ disable socket.socket to disable the Internet. useful in testing.

    .. doctest::
        >>> enable_socket()
        [!] socket.socket is enabled.
        >>> disable_socket()
        [!] socket.socket is disabled. Welcome to the desert of the real.
        >>> socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        Traceback (most recent call last):
        ...
        RuntimeError: I told you not to use the Internet!
        >>> enable_socket()
        [!] socket.socket is enabled.
        >>> enable_socket()
        [!] socket.socket is enabled.
        >>> disable_socket()
        [!] socket.socket is disabled. Welcome to the desert of the real.
        >>> socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        Traceback (most recent call last):
        ...
        RuntimeError: I told you not to use the Internet!
        >>> enable_socket()
        [!] socket.socket is enabled.
    """
    setattr(_module, '_socket_disabled', True)

    def guarded(*args, **kwargs):
        if getattr(_module, '_socket_disabled', False):
            raise RuntimeError("I told you not to use the Internet!")
        else:
            # SocketType is a valid public alias of socket.socket,
            # we use it here to avoid namespace collisions
            return socket.SocketType(*args, **kwargs)

    socket.socket = guarded

    print(u'[!] socket.socket is disabled. Welcome to the desert of the real.')


def enable_socket():
    """ re-enable socket.socket to enable the Internet. useful in testing.
    """
    setattr(_module, '_socket_disabled', False)
    print(u'[!] socket.socket is enabled.')

conftest.py

# Put this in the conftest.py at the top of your unit tests folder,
# so it's available to all unit tests
import pytest
import _socket_toggle


def pytest_runtest_setup():
    """ disable the interet. test-cases can explicitly re-enable """
    _socket_toggle.disable_socket()


@pytest.fixture(scope='function')
def enable_socket(request):
    """ re-enable socket.socket for duration of this test function """
    _socket_toggle.enable_socket()
    request.addfinalizer(_socket_toggle.disable_socket)

test_example.py

# Example usage of the py.test fixture in tests
import socket
import pytest

try:
    from urllib2 import urlopen
except ImportError:
    import urllib3
    urlopen = urllib.request.urlopen


def test_socket_disabled_by_default():
    # default behavior: socket.socket is unusable
    with pytest.raises(RuntimeError):
        urlopen(u'https://www.python.org/')


def test_explicitly_enable_socket(enable_socket):
    # socket is enabled by pytest fixture from conftest. disabled in finalizer
    assert socket.socket(socket.AF_INET, socket.SOCK_STREAM)

【讨论】:

  • 为什么不引发ConnectionError 异常?
  • @FemtoTrader 我想是因为这正确。不想将我们抛出的错误与内置程序抛出的错误混淆为合法的ConnectionError。在实践中我实际上使用了一个运行时错误的子类,但我想让这个例子更简单
  • 是否有可能更新适用于 Python3 的更新?我也很乐意帮助把它变成一个 pytest 插件。
  • @MikeFiedler 我已经更新了链接到您的插件的答案。干得好!
  • 出色的工作。通过我们设置测试的方式,这是一个很好的附加检查
【解决方案3】:

requests 库上加个噱头的简单方法:

from unittest import mock

requests_gag = mock.patch(
    'requests.Session.request',
    mock.Mock(side_effect=RuntimeError(
        'Please use the `responses` library to mock HTTP in your tests.'
    ))
)

with requests_gag:
    ...  # no Internet here


【讨论】:

    【解决方案4】:

    建立在 Thomas Orozco 和driftcatcher 的非常有用的答案的基础上,这里是一个可与 Python 的 unittest 和(稍作改动后)Django 一起使用的变体。

    您需要做的就是从增强的NoSocketTestCase 类继承您的测试用例类,任何对网络的访问都会被检测到并引发SocketAccessError 异常。

    这种方法也适用于 Django。您只需要将NoSocketTestCase 类更改为继承自django.test.TestCase 而不是unittest.TestCase

    虽然没有严格回答 OP 的问题,但我认为这可能对任何想要在单元测试中阻止网络访问的人有所帮助。

    no_sockets.py

    ​​>
    import socket
    from unittest import TestCase
    
    
    class SocketAccessError(Exception):
        pass
    
    
    class NoSocketsTestCase(TestCase):
        """Enhancement of TestCase class that prevents any use of sockets
    
        Will throw the exception SocketAccessError when any code tries to
        access network sockets
        """
    
        @classmethod
        def setUpClass(cls):
            cls.socket_original = socket.socket
            socket.socket = cls.guard
            return super().setUpClass()
    
        @classmethod
        def tearDownClass(cls):
            socket.socket = cls.socket_original
            return super().tearDownClass()
    
        @staticmethod
        def guard(*args, **kwargs):
            raise SocketAccessError('Attempted to access network')
    
    

    test_no_sockets.py

    ​​>
    import urllib.request
    from .no_sockets import NoSocketsTestCase, SocketAccessError
    
    
    class TestNoSocketsTestCase(NoSocketsTestCase):
    
        def test_raises_exception_on_attempted_network_access(self):
    
            with self.assertRaises(SocketAccessError):            
                urllib.request.urlopen('https://www.google.com')
    
    

    【讨论】:

      【解决方案5】:

      httpretty 是一个解决这个问题的小库。

      如果您使用的是 Django 测试运行程序,请编写一个自定义测试运行程序,在其中禁用所有第 3 方 API 调用。

      # common/test_runner.py
      
      import httpretty
      from django.test.runner import DiscoverRunner
      
      
      class CustomTestRunner(DiscoverRunner):
          def run_tests(self, *args, **kwargs):
              with httpretty.enabled(allow_net_connect=False):
                  return super().run_tests(*args, **kwargs)
      
      

      将此新的测试运行器添加到您的设置中

      TEST_RUNNER = "common.test_runner.CustomTestRunner"
      

      从现在开始,必须模拟所有外部 API 调用,否则将引发 httpretty.errors.UnmockedError

      如果你使用的是 pytest,这个夹具应该可以工作。

      @pytest.fixture
      def disable_external_api_calls():
          httpretty.enable()
          yield
          httpretty.disable()
      

      【讨论】:

        【解决方案6】:

        我有一个 pytest 解决方案。 pytest-network lybrary 帮我解决这个问题。

        # conftest.py
        import pytest
        import socket
        
        _original_connect = socket.socket.connect
        
        def patched_connect(*args, **kwargs):
            ...
            # It depends on your testing purpose
            # You may want a exception, add here
            # If you test unconnectable situations
            # it can stay like this 
            
        
        @pytest.fixture
        def enable_network():
            socket.socket.connect = _original_connect
            yield
            socket.socket.connect = patched_connect
        
        @pytest.fixture
        def disable_network():
            socket.socket.connect = patched_connect
            yield
            socket.socket.connect = _original_connect
        
        # test_internet.py
        def test_your_unconnectable_situation(disable_network):
            response = request.get('http://stackoverflow.com/')
            response.status_code == 400
        

        【讨论】:

          猜你喜欢
          • 2016-04-15
          • 2021-09-10
          • 2014-06-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-07-22
          • 2013-06-27
          • 1970-01-01
          相关资源
          最近更新 更多