Search code examples
python-3.xunit-testingboto3moto

Python mocking using MOTO for SSM


Taken from this answer:

Python mock AWS SSM

I now have this code:

test_2.py

from unittest import TestCase

import boto3
import pytest
from moto import mock_ssm


@pytest.yield_fixture
def s3ssm():
    with mock_ssm():
        ssm = boto3.client("ssm")
        yield ssm


@mock_ssm
class MyTest(TestCase):
    def setUp(self):
        ssm = boto3.client("ssm")
        ssm.put_parameter(
            Name="/mypath/password",
            Description="A test parameter",
            Value="this is it!",
            Type="SecureString",
        )

    def test_param_getting(self):
        import real_code

        resp = real_code.get_variable("/mypath/password")
        assert resp["Parameter"]["Value"] == "this is it!"

and this is my code to test (or a cut down example):

real_code.py

import boto3


class ParamTest:
    def __init__(self) -> None:
        self.client = boto3.client("ssm")
        pass

    def get_parameters(self, param_name):
        print(self.client.describe_parameters())
        return self.client.get_parameters_by_path(Path=param_name)


def get_variable(param_name):
    p = ParamTest()
    param_details = p.get_parameters(param_name)

    return param_details

I have tried a number of solutions, and switched between pytest and unittest quite a few times!

Each time I run the code, it doesn't reach out to AWS so it seems something is affecting the boto3 client, but it doesn't return the parameter. If I edit real_code.py to not have a class inside it the test passes.

Is it not possible to patch the client inside the class in the real_code.py file? I'm trying to do this without editing the real_code.py file at all if possible.

Thanks,


Solution

  • The get_parameters_by_path returns all parameters that are prefixed with the supplied path.
    When providing /mypath, it would return /mypath/password.
    But when providing /mypath/password, as in your example, it will only return parameters that look like this: /mypath/password/..

    If you are only looking to retrieve a single parameter, the get_parameter call would be more suitable:

    class ParamTest:
        def __init__(self) -> None:
            self.client = boto3.client("ssm")
            pass
    
        def get_parameters(self, param_name):
            # Decrypt the value, as it is stored as a SecureString
            return self.client.get_parameter(Name=param_name, WithDecryption=True)
    

    Edit: Note that Moto behaves the same as AWS in this. From https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.get_parameters_by_path:

    [The Path-parameter is t]he hierarchy for the parameter. [...] The hierachy is the parameter name except the last part of the parameter. For the API call to succeeed, the last part of the parameter name can't be in the path.