1

We have unit tests running via Pytest, which use a custom decorator to start up a context-managed mock echo server before each test, and provide its address to the test as an extra parameter. This works on Python 2.

However, if we try to run them on Python 3, then Pytest complains that it can't find a fixture matching the name of the extra parameter, and the tests fail.

Our tests look similar to this:

@with_mock_url('?status=404&content=test&content-type=csv')
def test_file_not_found(self, url):
    res_id = self._test_resource(url)['id']
    result = update_resource(None, res_id)
    assert not result, result
    self.assert_archival_error('Server reported status error: 404 Not Found', res_id)

With a decorator function like this:

from functools import wraps

def with_mock_url(url=''):
    """
    Start a MockEchoTestServer and call the decorated function with the server's address prepended to ``url``.
    """
    def decorator(func):
        @wraps(func)
        def decorated(*args, **kwargs):
             with MockEchoTestServer().serve() as serveraddr:
                 return func(*(args + ('%s/%s' % (serveraddr, url),)), **kwargs)
        return decorated
    return decorator

On Python 2 this works; the mock server starts, the test gets a URL similar to "http://localhost:1234/?status=404&content=test&content-type=csv", and then the mock is shut down afterward.

On Python 3, however, we get an error, "fixture 'url' not found".

Is there perhaps a way to tell Python, "This parameter is supplied from elsewhere and doesn't need a fixture"? Or is there, perhaps, an easy way to turn this into a fixture?

ThrawnCA
  • 1,051
  • 1
  • 10
  • 23
  • You could try using the `request` fixture https://stackoverflow.com/questions/69852075/pytest-fixture-with-argument/69867051#69867051 – Tzane Dec 02 '21 at 07:08
  • Thanks for the tip, but that appears to be about customising the creation of fixtures. In this case, creating the parameter is easy enough; the difficulty is passing it to the test as a function parameter that is *not* a fixture. – ThrawnCA Dec 02 '21 at 22:36

3 Answers3

1

You can use url as args parameter

@with_mock_url('?status=404&content=test&content-type=csv')
def test_file_not_found(self, *url):
    url[0] # the test url
Guy
  • 46,488
  • 10
  • 44
  • 88
  • That might work, but it would be clunky; every affected function would have to change its signature and some of its behaviour, and would have to make assumptions about parameter ordering that could change if the parameters ever change in the future. I'll keep it in mind as a backup option, but if I were going to go to that length, I'd probably just drop the decorator entirely and directly invoke the context manager in every test. – ThrawnCA Dec 02 '21 at 22:13
1

Looks like Pytest is content to ignore it if I add a default value for the injected parameter, to make it non-mandatory:

@with_mock_url('?status=404&content=test&content-type=csv')
def test_file_not_found(self, url=None):

The decorator can then inject the value as intended.

ThrawnCA
  • 1,051
  • 1
  • 10
  • 23
0

consider separating the address from the service of the url. Using marks and changing fixture behavior based on the presence of said marks is clear enough. Mock should not really involve any communication, but if you must start some service, then make it separate from

with_mock_url = pytest.mark.mock_url('http://www.darknet.go')


@pytest.fixture
def url(request):
    marker = request.get_closest_marker('mock_url')
    if marker:
        earl = marker.args[0] if args else marker.kwargs['fake']
        if earl:
            return earl

    try:
        #
        earl = request.param
    except AttributeError:
        earl = None


    return earl

@fixture
def server(request):
    marker = request.get_closest_marker('mock_url')
    if marker:
        # start fake_server


@with_mock_url
def test_resolve(url, server):
    server.request(url)
    
msudder
  • 505
  • 3
  • 14