NJ is on Codidact
NJ is on Codidact

Reputation: 1091

pytest: How to test project-dependent directory creation?

I'm developing a project that has the following architecture:

|-- weather
|   |-- __init__.py
|   |-- weather.py
|-- library
|   |-- data.json
|-- test
|   |-- __init__.py
|   |-- test_weather.py

I would like to test that performing a save creates the library directory if it does not already exist. test_weather.py contains:

import os
import shutil

from weather import weather

def test_library_creation():
    # Initialize.
    app = weather.WeatherApp()
    temp_data = app.get_data()
    # Library should not exist initially.
    assert not os.path.isdir('library')
    app.save(temp_data)
    # Library should exist after save.
    assert os.path.isdir('library')
    assert os.path.isfile(os.path.join('library', 'data.json'))
    # Cleanup.
    shutil.rmtree('library')

However, I may have some data saved in data.json that I don't want to delete as a result of running this test.

Does pytest have a solution for this case?


EDIT :

weather.py contains:

import os
import json

DEFAULT_SAVE_PATH = os.path.join('library', 'data.json')

class WeatherApp:
    def get_data(self):
        return dict(temperature=25, humidity=0.5)

    def save(self, data, save_path=DEFAULT_SAVE_PATH):
        with open('data.json', 'w') as jf:
            json.dump(data, jf)
        os.renames('data.json', save_path)

Upvotes: 2

Views: 1570

Answers (1)

hoefling
hoefling

Reputation: 66431

Since you have already thought about providing a custom save path, you don't even need to mock anything if you don't want to; just pass the custom path derived from the tmp_path fixture:

def test_library_creation(tmp_path):
    app = weather.WeatherApp()
    temp_data = app.get_data()
    lib_dir = tmp_path / 'library'
    # this check is somewhat redundant because pytest will ensure
    # the temp dir is created from scratch and is empty
    assert not lib_dir.is_dir()
    app.save(temp_data, save_path=str(lib_dir / 'data.json'))
    # Library should exist after save.
    assert lib_dir.is_dir()
    assert (lib_dir / 'data.json').is_file()
    shutil.rmtree(str(lib_dir))

Some notes:

  1. The string conversion of path-like objects is required only on Python versions older than 3.6; if you use 3.6 or 3.7, you can work directly with path-like objects, e.g.

    app.save(temp_data, save_path=lib_dir / 'data.json')
    

    or

    shutil.rmtree(lib_dir)
    
  2. Beware that os.rename/os.renames are not error-prone against changing filesystems, e.g. you write data.json on a local ext4 partition and save_path points to a CIFS share, and here comes the error.

  3. Maybe the renaming operation is redundant? You can write the data directly to save_path. You just need to ensure the target dir exists first, e.g. with os.makedirs(os.path.dirname(save_path), exist_ok=True).

  4. If an assertion fails in the test, the line shutil.rmtree(str(lib_dir)) won't be executed; this is no big deal since the tmp_path is created on tmpfs and will be removed after the next reboot anyway. However, if you want to handle the deletion yourself, I would do this in the test teardown using a custom fixture:

    import os
    import shutil
    import pytest
    
    from weather import weather
    
    
    @pytest.fixture
    def lib_dir(tmp_path):
        d = tmp_path / 'library'
        yield d
        shutil.rmtree(str(d))
    
    
    def test_library_creation(lib_dir):
        app = weather.WeatherApp()
        temp_data = app.get_data()
        assert not lib_dir.is_dir()
        app.save(temp_data, save_path=str(lib_dir))
        # Library should exist after save.
        assert lib_dir.is_dir()
        assert (lib_dir / 'data.json').is_file()
    

    Now the lib_dir will be removed regardless whether the test passes or not.

  5. If you want to test the default arguments (the app.save(temp_data) case), you will need to monkeypatch the DEFAULT_SAVE_PATH constant in the weather module. It's pretty easy using the monkeypatch fixture:

    def test_library_creation(monkeypatch, lib_dir):
        app = weather.WeatherApp()
        temp_data = app.get_data()
        assert not lib_dir.is_dir()
        with monkeypatch.context() as m:
            m.setattr(weather, 'DEFAULT_SAVE_PATH', os.path.join(lib_dir, 'data.json'))
            app.save(temp_data)
        # Library should exist after save.
        assert lib_dir.is_dir()
        assert (lib_dir / 'data.json').is_file()
    

Upvotes: 3

Related Questions