0

I'm struggling with (and new to) mocking. I found several StackOverflow posts that ask questions about creating mock objects for testing functions that use subprocess however none have helped me understand how the mock objects workflow, works.

Example post used below as a guide: Mocking a subprocess call in Python

In the example below, I try to MOCK a _call_git helper function that makes a subprocess.run call to run git clone. There is something fundamental about how this works that I don't understand.

Here are my functions

# Generic helper to call git for commands that github3.py doesn't wrap
def _call_git(*args, directory=None):
    cmd = ["git"]
    cmd.extend(args)
    try:
        ret = subprocess.run(
            cmd,
            cwd=directory,
            check=True,
            universal_newlines=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
    except subprocess.CalledProcessError as e:
        err = e.stderr
        if not err:
            err = e.stdout
        raise RuntimeError(err) from e

    return ret

# Function to clone a repository and uses _call_git helper
def clone_repo(organization, repo, dest_dir):
    try:
        # first, check that local git set up with ssh keys for github
        check_git_ssh()
        url = "git@github.com:{}/{}.git".format(organization, repo)

        _call_git("-C", dest_dir, "clone", url)
        print("Successfully cloned:", url)
    except RuntimeError as e:
        print(
            r"Oops, something went wrong when cloning {}\{}: \n".format(
                organization, repo
            )
        )
        raise e

I want to test that the clone worked. But I don't want to run git clone and setup SSH in my Continuous Integration build. I can just patch around it and test that errors are raised as I expect.

@mock.patch("subprocess.run")
def test_clone_repo_pass2(
        mock_subproc_run,
        monkeypatch, capsys
):

    # Replace check_github_ssh with a pass 
    monkeypatch.setattr(github, "check_git_ssh", mock_check_ssh)

    # Mock the subprocess call - This is in the example SO post. But i can comment it out and the test still runs. Why?
    #process_mock = mock.Mock()
    #attrs = {"communicate.return_value": ("output", "error")}
    #process_mock.configure_mock(**attrs)
    #mock_subproc_run.return_value = process_mock
    # This passes regardless of what organization / repo combination I provide. So it's being patched, but with what? A "pass" or skipping running that subprocess call altogether?
    github.clone_repo(
        organization="earth55lab", repo="earthpy", dest_dir="assignment-1"
    )
    captured = capsys.readouterr()
    lines = captured.out.splitlines()
    assert lines[1].startswith("Successfully cloned: git@github")

My questions are:

  1. How does the moc_subprocess_run object behave when attributes / methods etc of the returned MagicMock object isn't provided (ie notice i can comment out 4 lines of code that define attributes and the test still passes). Does it just pass because the MagicMock returned object doesn't do anything and keep going?
  2. Is it reasonable to achieve the same results by creating a mock function that returns a "pass" via monkeypatch.setattr(github, "_call_git", mock_call_git) ?
  3. Finally how do I best decide what approach to take when i'm setting up mock tests? Do I use pytest monkeypatch fixture vs unittest mock + patch? Or is it purely preference that helps me decide?

I think conceptually i'm struggling with there being multiple ways to patch and mock functions that make calls to online API's. I'm not sure how to decide what approach is best.

Thank you for any guidance. I will then post my final tests once I have this all working via an update to this post in hopes this helps others.

Leah Wasser
  • 695
  • 3
  • 8
  • 22

0 Answers0