Jacob Pavlock
Jacob Pavlock

Reputation: 751

How to test a single call within a method?

In the following code, I'd like to test that if config_dir doesn't exist, it is created.

    def __init__(
        self, config_dir: pathlib.Path = pathlib.Path().home() / ".config" / "moe",
    ):
        """Read configuration.

        Args:
            config_dir: Path of the configuration directory.
        """
        self.config_dir = config_dir

        if not self.config_dir.exists():
            self.config_dir.mkdir(parents=True)

        self.db_path: pathlib.Path = self.config_dir / "library.db"

        # initialize db
        engine = sqlalchemy.create_engine("sqlite:///" + str(self.db_path))
        library.Session.configure(bind=engine)
        library.Base.metadata.create_all(engine)  # create tables if they don't exist

However, I can't seem to figure out how to just test that part of the code. I've tried

    def test_config_dir_dne(self, mocker):
        """We should create the config directory if it doesn't exist."""
        fake_path = mocker.Mock()
        moe.config.Config(fake_path)

        fake_path.mkdir.assert_called_once_with(parents=True)

But, pathlib doesn't like that I use the specific join operator with the mock.

>       self.db_path: pathlib.Path = self.config_dir / "library.db"
E       TypeError: unsupported operand type(s) for /: 'Mock' and 'str'

Also, I don't really care about that part of the code for this test, nor do I wish to create the engine at the end.

How can I just test check that mkdirs is called on my fake_path without running any of the other code?

Upvotes: 0

Views: 119

Answers (2)

chepner
chepner

Reputation: 532268

Add an additional argument to __init__, the function used to initialize the database. By default, you'll use a function supplied by the class itself.

def __init__(
    self,
    config_dir: pathlib.Path = pathlib.Path().home() / ".config" / "moe",
    db_initializer=None
):
    """Read configuration.

    Args:
        config_dir: Path of the configuration directory.
    """
    self.config_dir = config_dir

    if not self.config_dir.exists():
        self.config_dir.mkdir(parents=True)

    self.db_path: pathlib.Path = self.config_dir / "library.db"

    if db_initializer is None:
        db_intializer = self._init_db

    db_initializer(self.dp_path)

@staticmethod
def _init_db(p):
    engine = sqlalchemy.create_engine("sqlite:///" + str(p))
    library.Session.configure(bind=engine)
    library.Base.metadata.create_all(engine)

When you test the function, you'll pass a do-nothing function instead.

def test_config_dir_dne(self, mocker):
        """We should create the config directory if it doesn't exist."""
        fake_path = mocker.Mock()
        moe.config.Config(fake_path, lambda p: None)

        fake_path.mkdir.assert_called_once_with(parents=True)

Upvotes: 0

Samwise
Samwise

Reputation: 71562

There are three general approaches here:

  1. Mock out more. Specifically, you could patch out pathlib.Path and sqlalchemy.create_engine and everything else that you don't want to test.

  2. Use dependency injection so you can mock without patching. Have the constructor take abstract interfaces and then have your test pass in dummy versions of those.

  3. Restructure your code so that each unit you want to test on its own is its own function. Take the logic that you want to test (the creation of the config dir), put that into its own function, and then unit-test that function independently of your constructor.

Upvotes: 2

Related Questions