Search code examples
pythonpython-3.xpython-unittestpython-mock

Mocking a function/object, and return values based on input/conditions


I am facing the following scenario.

I have the following source code

#file:[src/my_module.py]

def order_names(unordered_input: List) -> str: 
   # function that orders a list of names
   ...
       if(is_ID(unordered_input[i])):
           id = unordered_input[i]
           name = get_name_by_id(id)
   ...

def get_name_by_id(id) -> str: 
   # function that returns a name, based on an ID, through a Rest API call
   return make_some_network_call(id)

I want to test the function order_names, and I want to mock the calls to get_name_by_id(id).

Assuming that the get_name_by_id(id) will be called many times for various ids, can a mock be created that returns values according to the input?

For example:

#file:[test/test_my_module.py]
from unittest import mock
from my_module import order_names

@mock.patch("src.my_module.get_name_by_id", return_value={"3": "Mark", "4": "Kate", "5":"Alfred"})
def test_order_names():
   ordered_names = order_names(["3", "4", "Suzan", "5"])
   assert ordered_names == "Alfred, Kate, Mark, Suzan"

The above test code is an example of the type of the behavior to be achieved, since get_name_by_id() is not a dict return type.

Cheers!


Solution

  • You basically need an alternate implementation of get_name_by_id, not just a new return value.

    # Adjust the definition to behave the same when the lookup fails
    def get_name_locally(id):
        return {"3": "Mark", "4": "Kate", "5":"Alfred"}.get(id)
    
    
    def test_order_names():
        with mock.patch('src.my_module.get_name_by_id', get_name_locally):
            ordered_names = order_names(["3", "4", "Suzan", "5"])
        assert ordered_names = "Alfred, Kate, Mark, Suzan"
    

    If get_name_by_id is more complicated, you could also consider patching the network call instead and letting get_name_by_id run as-is.

    # The same as get_name_locally above, but only because
    # get_name_by_id and make_some_network_call are functionally
    # identical as far as the question is written.
    def network_replacement(id):
        return {"3": "Mark", "4": "Kate", "5":"Alfred"}.get(id)
    
    
    def test_order_names():
        with mock.patch('src.my_module.make_some_network_call', network_replacement):
            ordered_names = order_names(["3", "4", "Suzan", "5"])
        assert ordered_names = "Alfred, Kate, Mark, Suzan"
    

    Now when you call order_names, and it calls get_name_by_id, your alternate definition of make_some_network_call will be used by get_name_by_id.