Robert Calceanu
Robert Calceanu

Reputation: 91

How do I implement pytest for a function with @contextlib.contextmanager?

How do I implement pytest for a function with @contextlib.contextmanager ?

In order to improve the coverage, I want to have a test for this function as well.

@contextlib.contextmanager
def working_directory_relative_to_script_location(path):
    """Changes working directory, and returns to previous on exit. It's needed for PRAW for example,
    because it looks for praw.ini in Path.cwd(), but I have that file in the settings directory.

    """
    prev_cwd = Path.cwd()
    script_dir = Path(os.path.realpath(__file__)).parent
    os.chdir(script_dir / path)
    try:
        yield
    finally:
        os.chdir(prev_cwd)

Upvotes: 0

Views: 740

Answers (3)

BorjaEst
BorjaEst

Reputation: 755

This answer does not strictly solve the title of the question but it offers a better solution for the typical problem it addresses.

Use fixtures to prepare your test context.

You can easily change to a directory in a test context creating a fixture.

@fixture(scope="module", autouse=True, params=["./settings"])
def in_settings_folder(request.param):
    prevdir = os.getcwd()
    os.chdir(request.param)
    yield
    os.chdir(prevdir)

def test_settings_1(): # This pass
    print(f"{os.getcwd()}") # /.../project/settings
    assert check_settings()

def test_settings_2(): # This fails
    print(f"{os.getcwd()}") # /.../project/settings
    raise Exception

def test_settings_3(): # This pass
    print(f"{os.getcwd()}") # /.../project/settings
    assert check_settings()

I changed it a bit to use tmpdir, but you will see something like:

$ pytest -s tests/test_tests.py 
...
tests/test_tests.py /tmp/pytest-of-borja/pytest-52/settings
./tmp/pytest-of-borja/pytest-52/settings
F/tmp/pytest-of-borja/pytest-52/settings
.
...

Where you can see on the 3 tests, even if 1 failed, the cwd is maintained.

Control the scope of your fixture

You can decide when this directory "switch" will happen using the fixture scope. Note if you use autouse with session all the tests will run in that directory.

You can use scope=module if you want to apply it to all tests in the module where it is called or scope=class inside a Class to limit it to the test inside the class:

class TestSettings:
    @fixture(scope="class", autouse=True, params=["./settigns"])
    def in_settings_folder(self, tmpdir):
        prevdir = os.getcwd()
        os.chdir(tmpdir)
        yield
        os.chdir(prevdir)

    def test_settings_1(self): 
        print(f"{os.getcwd()}")  # /.../project/settings
        assert check_settings()

def test_settings_2():           # /.../project
    print(f"{os.getcwd()}")
    assert check_settings()

Use pytest temp folders to operate data

If your fixtures are generating data which you want to check on the tests, then use tmpdir_factory to create a working directory.

@fixture(scope="module", autouse=True)
def tmpdir(tmpdir_factory):
    return tmpdir_factory.mktemp("settings")


class TestSettings:
    @fixture(scope="class", autouse=True)
    def in_settings_folder(self, tmpdir):
        prevdir = os.getcwd()
        os.chdir(tmpdir)
        yield
        os.chdir(prevdir)

    def test_settings(self):
        print(f"{os.getcwd()}") # /tmp/pytest-of-x/pytest-1/settings0
        assert check_settings()

Upvotes: 0

Robert Calceanu
Robert Calceanu

Reputation: 91

Shortest version:

def test_working_directory_relative_to_script_location(tmpdir):
    initial_path = Path.cwd()

    @working_directory_relative_to_script_location(tmpdir)
    def with_decorator():
        return Path.cwd()

    try:
        assert with_decorator() == tmpdir

Upvotes: 0

Michael H.
Michael H.

Reputation: 3483

Maybe not the nicest solution because it actually creates directories on your drive:

import contextlib
import os
from pathlib import Path

@contextlib.contextmanager
def working_directory_relative_to_script_location(path):
    """Changes working directory, and returns to previous on exit. 
    It's needed for PRAW for example,
    because it looks for praw.ini in Path.cwd(), 
    but I have that file in the settings directory."""
    prev_cwd = Path.cwd()
    script_dir = Path(os.path.realpath(__file__)).parent
    os.chdir(script_dir / path)
    try:
        yield
    finally:
        os.chdir(prev_cwd)


def test_decorator():
    tmp = 'tmp_dir'
    initial_path = Path.cwd()
    os.mkdir(tmp)
    tmp_path = os.path.join(initial_path, tmp)

    @working_directory_relative_to_script_location(tmp_path)
    def with_decorator():
        return Path.cwd()

    try:
        assert with_decorator() == tmp_path
        assert Path.cwd() == initial_path
    except AssertionError as e:
        raise e
    finally:
        os.rmdir(tmp)

test_decorator()

Here, I created a function that returns the current working directory and decorated it with your context manager. What one would expect from your context manager is that it changes the directory to tmp during the function invocation (this is tested by the first assert statement) and that it changes it back to the initial directory afterwards (this is tested by the second assert statement).

Upvotes: 1

Related Questions