Search code examples
pythonamazon-web-servicespython-unittestpython-unittest.mock

How to mock config variables in python3?


This is a AWS lambda function

#service.py
from configs import SENDER, DESTINATIONS
from constants import LOG_FORMAT
import logging

def send_mail(body, SENDER, DESTINATIONS):
    ...
    ...

In config files it's retrieving data from AWS param store

# configs.py
from handlers.ssm_handler import load_parameters
from common import constants
import os
environment = os.environ.get(constants.ENVIRONMENT)

JSON_BUCKET = load_parameters(constants.OT_ARCHIVAL_PREFIX+environment+constants.MIGRATION_BUCKET)
SENDER = load_parameters(constants.OT_ARCHIVAL_PREFIX+environment+constants.MAIL_SENDER)
DESTINATIONS = load_parameters(constants.OT_ARCHIVAL_PREFIX+environment+constants.MAIL_DESTINATIONS)
...

So when i try to test it

# test_service.py
from unittest import TestCase, main, mock
from service import send_mail

class TestMailService(TestCase):
     def test_service(self):
       with mock.patch('service.SENDER', '[email protected]') as mocked_sender:
         with mock.patch('service.DESTINATIONS', '[email protected]') as mocked_sender:
           with mock.patch('service.logging.Logger.info') as mocked_logging:
              send_mail(...)
              mocked_logging.assert_called_with('mail sent Successfully')

This test case is passing when I export AWS Security credentials. But it'wont, if i don't pass the credentials. I guess it's because in the service.py file it's opening the entire config.py file. So it would require sec credentials to call AWS. As a solution I tried mocking SENDER and DESTINATIONS. But it throws me error(expecting security tokens)

I want the unittest to be security token independent. Suggest a solution


Solution

  • It happens because when you import configs.py e.g. via from configs import SENDER, DESTINATION, it would automatically run those statements that call load_parameters which in turn calls AWS SSM even if there are no active mocks/patches yet.

    Solution 1

    Try refactoring configs.py in a way that the setting of the variables would only happen upon an explicit call (and not upon import). The simplest implementation would be something like:

    configs.py

    import os
    
    from common import constants
    from handlers.ssm_handler import load_parameters
    
    
    def get_params():
        environment = os.environ.get(constants.ENVIRONMENT)
        return {
            "SENDER": load_parameters(constants.OT_ARCHIVAL_PREFIX+environment+constants.MAIL_SENDER),
            "DESTINATIONS": load_parameters(constants.OT_ARCHIVAL_PREFIX+environment+constants.MAIL_DESTINATIONS),
        }
    

    This would need some refactoring as the call to get_params must be inserted at the start of the AWS Lambda function call. This way, the calling of load_parameters which in turn uses AWS SSM would not be executed automatically and we can prepare our mocks/patches before it is called.

    Solution 2

    Don't import any file that would in turn import configs.py while there is no active mock/patch yet. Patch load_parameters first so that it doesn't connect to the actual AWS SSM. You can patch it manually or you can use the decorator @mock_ssm from moto. Only then we can safely import the files.

    from unittest import TestCase, main, mock
    
    from moto import mock_ssm
    
    # from service import send_mail  # REMOVE THIS IMPORT!
    
    
    @mock_ssm  # Option 1. Requires <pip install moto>. You have to setup SSM first as usual.
    def test_service(mocker):  # Requires <pip install pytest-mock>
        mocker.patch('handlers.ssm_handler.load_parameters')  # Option 2
        # with mock.patch('handlers.ssm_handler.load_parameters') as mock_ssm:  # Option 3. This is equivalent to Option 2.
    
        mocker.patch('service.SENDER', '[email protected]')
        mocker.patch('service.DESTINATIONS', '[email protected]')
    
        from service import send_mail  # Import it here after the patches have taken effect
        send_mail(...)