1

I have a file seed_dynamodb.py whose code is below. I want to write a unit test for it using mock patch. I am successfully able to patch boto3. Now I need to patch sys.argv as well. I have tried with below test code but it is giving an IndexError.

==========seed_dynamodb.py==========

import sys
import boto3
def main(env,region):

    dynamodb_client= boto3.client('dynamodb')
    timestamp = '1234567'
    table_name = 'syn-fcad-nielsen-' + env + '-time'
    print(f'{table_name=}')
    if env == 'uat':
     timestamp = 1234567
    if env == 'prod':
     timestamp = 1234567
    response = dynamodb_client.get_item(TableName=table_name, 
                                        Key={'BaseTime':{'S':'Timestamp'}})
    if 'Item' in response:
        print("Item exists in Dynamo DB table")
        timestamp = response['Items']['Value']['N']
    else: 
        response = dynamodb_client.put_item(TableName=table_name, 
                    Item={
                    'BaseTime':{'S':'Timestamp'},
                    'Value': {'N': timestamp}
                    })

env = sys.argv[1]
region = sys.argv[2]
l = len(sys.argv)
print(f'{env=}{region=}{l=}')
main(env,region)

=======================test_dynamo.py=========

from module import seed_dynamodb
import unittest
from unittest import mock
from unittest.mock import patch
import boto3
import sys

@mock.patch("module.seed_dynamodb.boto3.client")
class SeedDynamoDBTest(unittest.TestCase):
    @patch.object(boto3, "client")
    @patch.object(sys, 'argv', ["pr", "us-east-1"])
    def test_seed_dynamodb(self, *args):
        mock_boto = args[0]
        mock_dynamo = args[0]
        mock_dynamo.get_item.return_value = {"Item": {"Value": {"N": 1678230539}}}
        mock_dynamo.put_item.return_value = {"Item": {"BaseTime": {"S": "Timestamp"}, "Value": {"N": 1678230539}}}
        seed_dynamodb.dynamodb_client = mock_dynamo
        self.assertIsNotNone(mock_boto.client.get_item.return_value)
        # seed_dynamodb.main("pr-173", "us-east-1")
        
if __name__ == "__main__":
    unittest.main(verbosity=2)

I am getting below issue:

env = sys.argv[1]
IndexError: list index out of range

Can you please help me how i can fix this issue or write test case for patching sys.argv

frankfalse
  • 1,553
  • 1
  • 4
  • 17
Deepak Gupta
  • 387
  • 2
  • 17

3 Answers3

1

Lets try to patch sys.argv directly instead of using patch.object Here would be your updated code:

from module import seed_dynamodb
import unittest
from unittest import mock
from unittest.mock import patch
import boto3
import sys

@mock.patch("module.seed_dynamodb.boto3.client")
class SeedDynamoDBTest(unittest.TestCase):
    @patch.object(boto3, "client")
    def test_seed_dynamodb(self, mock_boto):
        mock_dynamo = mock_boto.return_value
        mock_dynamo.get_item.return_value = {"Item": {"Value": {"N": 1678230539}}}
        mock_dynamo.put_item.return_value = {
            "Item": {"BaseTime": {"S": "Timestamp"}, "Value": {"N": 1678230539}}
        }
        with patch("sys.argv", ["script_name", "pr", "us-east-1"]):
            seed_dynamodb.main(sys.argv[1], sys.argv[2])
        self.assertIsNotNone(mock_dynamo.client.get_item.return_value)

if __name__ == "__main__":
    unittest.main(verbosity=2)

This should allow you to test the functionality of seed_dynamodb.main

Saxtheowl
  • 4,136
  • 5
  • 23
  • 32
0

Don't make your code depend so directly on sys.argv in the first place. Instead of

env = sys.argv[1]
region = sys.argv[2]
l = len(sys.argv)
print(f'{env=}{region=}{l=}')
main(env,region)

do something like

def parse_args(argv=None):
    p = argparse.ArgumentParser()
    p.add_argument('env')
    p.add_argument('region')
    return p.parse_args(argv)

if __name__ == '__main__':
    args = parse_args()
    main(args.env, args.region)

Now you can test parse_args using whatever list you like, instead of depending on sys.argv or needing to patch anything.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • I am not sure if I can do that because I have to call this script from jesnkins pipeline in shell script – Deepak Gupta Apr 04 '23 at 12:59
  • This doesn't change the public interface of the script at all, only how it processes `sys.argv`. (`p.parse_args`, if it receives an argument of `None`, uses `sys.argv[1:]` as the list of arguments to parse.) When you *test* `parse_args`, you just pass an explicit list of your choosing instead of relying `sys.argv`. – chepner Apr 04 '23 at 13:02
  • I tried it. I changed code of seed_dynamodb.py as per your suggestion but when i also changed code of test_dynamodb.py and now i get below issue usage: test_dynamodb.py [-h] env region test_dynamodb.py: error: the following arguments are required: env, region – Deepak Gupta Apr 04 '23 at 19:28
  • The first element of `sys.argv` is the name of the command, not the first argument passed *to* that command. – chepner Apr 04 '23 at 19:38
  • that i know that is why i was using sys.argv[1] – Deepak Gupta Apr 04 '23 at 19:43
  • But your *patched* value only contains two values, `'pr'` and `'us-east-1'`, not that your test needs to patch anything with this approach. You simply call `main('pr', 'us-east-1')` or `parse_args(['...', 'pr', 'us-east-1'])` as desired. – chepner Apr 04 '23 at 19:59
0

I have tried to modify your code to be execute and solved the IndexError: list index out of range.
To try the test code in my system I have made some changes to your files. All the changes are highlighted by some comments.
Essentially I have changed the position of your import: from module import seed_dynamodb. Furthermore (not highlighted by the comments) I have changed your print(f"{..}") instructions because they were not correct.

==========seed_dynamodb.py==========

import sys
import boto3

def main(env, region):
    dynamodb_client= boto3.client('dynamodb')
    timestamp = '1234567'
    table_name = 'syn-fcad-nielsen-' + env + '-time'
    print(f'table_name = {table_name}')
    if env == 'uat':
        timestamp = 1234567
    if env == 'prod':
        timestamp = 1234567
    response = dynamodb_client.get_item(TableName=table_name, 
                                        Key={'BaseTime':{'S':'Timestamp'}})
    if 'Item' in response:
        print("Item exists in Dynamo DB table")
        timestamp = response['Items']['Value']['N']
    else: 
        response = dynamodb_client.put_item(TableName=table_name, 
                    Item={
                    'BaseTime':{'S':'Timestamp'},
                    'Value': {'N': timestamp}
                    })

#env = sys.argv[1]      # <-------- your code (you will set index to 1 in your 
                        #           definitive code)
env = sys.argv[0]       # <-------- my code

#region = sys.argv[2]   # <-------- your code (you will set index to 2 in your
                        #           definitive code)
region = sys.argv[1]    # <-------- my code

l = len(sys.argv)
print(f'env={env}, region={region}, l={l}') # <--- change a bit the print(f"...")
main(env, region)

=======================test_dynamo.py=========

#from module import seed_dynamodb    # <-------- your code
import unittest
from unittest import mock
from unittest.mock import patch
import boto3
import sys

@mock.patch("module.seed_dynamodb.boto3.client")
class SeedDynamoDBTest(unittest.TestCase):
    @patch.object(boto3, "client")
    @patch.object(sys, 'argv', ["pr", "us-east-1"])
    def test_seed_dynamodb(self, *args):
        from module import seed_dynamodb    # <-------- my code
        mock_boto = args[0]
        #mock_dynamo = args[0]  # <-------- your code
        mock_dynamo = args[1]   # <-------- my code
        mock_dynamo.get_item.return_value = {"Item": {"Value": {"N": 1678230539}}}
        mock_dynamo.put_item.return_value = {
            "Item": {"BaseTime": {"S": "Timestamp"}, "Value": {"N": 1678230539}}
        }
        seed_dynamodb.dynamodb_client = mock_dynamo
        self.assertIsNotNone(mock_boto.client.get_item.return_value)
        #seed_dynamodb.main("pr-173", "us-east-1")

if __name__ == "__main__":
    unittest.main(verbosity=2)

This is the output of the execution of the test in my system:

test_seed_dynamodb (__main__.SeedDynamoDBTest) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.002s

OK
env=pr, region=us-east-1, l=2
table_name = syn-fcad-nielsen-pr-time

I hope this answer could be useful for you.

frankfalse
  • 1,553
  • 1
  • 4
  • 17