1

I have a question about how to mock a nested method and test what it was called with. I'm having a hard time getting my head around: https://docs.python.org/3/library/unittest.mock-examples.html#mocking-chained-calls.

I'd like to test that the "put" method from the fabric library is called by the deploy_file method in this class, and maybe what values are given to it. This is the module that gathers some information from AWS and provides a method to take action on the data.

import json
import os

from aws.secrets_manager import get_secret
from fabric import Connection


class Deploy:
    def __init__(self):
        self.secrets = None
        self.set_secrets()

    def set_secrets(self):
        secrets = get_secret()
        self.secrets = json.loads(secrets)

    def deploy_file(self, source_file):
        with Connection(host=os.environ.get('SSH_USERNAME'), user=os.environ.get("SSH_USERNAME")) as conn:
            destination_path = self.secrets["app_path"] + '/' + os.path.basename(source_file)
            conn.put(source_file, destination_path)

"get_secret" is a method in another module that uses the boto3 library to get the info from AWS.

These are the tests I'm working on:

from unittest.mock import patch
from fabric import Connection
from jobs.deploy import Deploy


def test_set_secrets_dict_from_expected_json_string():
    with patch('jobs.deploy.get_secret') as m_get_secret:
        m_get_secret.return_value = '{"app_path": "/var/www/html"}'
        deployment = Deploy()
        assert deployment.secrets['app_path'] == "/var/www/html"


def test_copy_app_file_calls_fabric_put():
    with patch('jobs.deploy.get_secret') as m_get_secret:
        m_get_secret.return_value = '{"app_path": "/var/www/html"}'
        deployment = Deploy()
        with patch('jobs.deploy.Connection', spec=Connection) as m_conn:
            local_file_path = "/tmp/foo"
            deployment.deploy_file(local_file_path)
            m_conn.put.assert_called_once()

where the second test results in "AssertionError: Expected 'put' to have been called once. Called 0 times."

the first test mocks the "get_secret" function just fine to test that the constructor for "Deploy" sets "Deploy.secrets" from the fake AWS data.

In the second test, get_secrets is mocked just as before, and I mock "Connection" from the fabric library. If I don't mock Connection, I get an error related to the "host" parameter when the Connection object is created.

I think that when "conn.put" is called its creating a whole new Mock object and I'm not testing that object when the unittest runs. I'm just not sure how to define the test to actually test the call to put.

I'm also a novice at understanding what to test (and how) and what not to test as well as how to use mock and such. I'm fully bought in on the idea though. It's been very helpful to find bugs and regressions as I work on projects.

Utegrad
  • 73
  • 7
  • 1
    The second example is the one to work on, you shouldn't patch the unit you're supposed to be testing. `m_conn` is the mock of the Connection class, which is *not* the same as the context manager `conn` (the result of instantiating the class and calling `__enter__` on the instance). See e.g. https://docs.python.org/3/library/unittest.mock.html#mocking-magic-methods – jonrsharpe Jun 20 '19 at 21:51
  • `m_conn` is just the class, not the context manager's result - you can give it `__enter__` and `__exit__` methods, have the enter return another mock and use that one to test for its `put` call. – jbndlr Jun 20 '19 at 21:51

0 Answers0