dumbledad
dumbledad

Reputation: 17555

Why is function under test initializing class instead of using the mock in pytest?

I am writing a unit test for the following function, and I'm looking first at the case where we reach the last line.

from azureml.core.authentication import InteractiveLoginAuthentication, ServicePrincipalAuthentication

def get_authentication() -> Union[InteractiveLoginAuthentication, ServicePrincipalAuthentication]:
    service_principal_id = get_secret_from_environment(SERVICE_PRINCIPAL_ID, allow_missing=True)
    tenant_id = get_secret_from_environment(TENANT_ID, allow_missing=True)
    service_principal_password = get_secret_from_environment(SERVICE_PRINCIPAL_PASSWORD, allow_missing=True)
    if service_principal_id and tenant_id and service_principal_password:
        return ServicePrincipalAuthentication(
            tenant_id=tenant_id,
            service_principal_id=service_principal_id,
            service_principal_password=service_principal_password)
    logging.info("Using interactive login to Azure. To use Service Principal authentication")
    return InteractiveLoginAuthentication()

And here is my very simple unit test:

@patch("azureml.core.authentication.InteractiveLoginAuthentication")
def test_get_authentication(mocked_interactive_authentication: Any) -> None:
    util.get_authentication()
    assert mocked_interactive_authentication.called

We never reach the assert line of my unit test because InteractiveLoginAuthentication() raises the exception

TypeError: super() argument 1 must be type, not MagicMock

Why is get_authentication calling the actual constructor for InteractiveLoginAuthentication and not using my patch?

Upvotes: 1

Views: 1196

Answers (2)

Niel Godfrey P. Ponciano
Niel Godfrey P. Ponciano

Reputation: 10719

It is not using your patch because the one that you patched is the root source class, which isn't the version already imported by your util at the time the test run was started. This means that the imported InteractiveLoginAuthentication within util remains the same and not the patched version.

Update:

My answer has been a bit late :D On top of the answer from @dumbledad which is to patch the imported InteractiveLoginAuthentication of the util instead of the root source class, another approach is by reloading the util. After the patch has been made to the root source class, reloading the util module would then reflect the patch made.

azureml/core/authentication.py

class InteractiveLoginAuthentication:
    def __init__(self):
        print("Actual InteractiveLoginAuthentication constructed!")

util.py

from azureml.core.authentication import InteractiveLoginAuthentication

def get_authentication() -> InteractiveLoginAuthentication:
    print("Using interactive login to Azure. To use Service Principal authentication")
    return InteractiveLoginAuthentication()

test_util.py

import sys
from typing import Any

from unittest.mock import patch

import util


@patch("azureml.core.authentication.InteractiveLoginAuthentication")
def test_get_authentication_2(mocked_interactive_authentication: Any) -> None:
    from importlib import reload
    reload(sys.modules['util'])

    util.get_authentication()
    assert mocked_interactive_authentication.called

Output:

$ pytest -rP
===================================================================================== test session starts ======================================================================================
collected 1 item                                                                                                                                                                               

test_util.py .                                                                                                                                                                           [100%]

============================================================================================ PASSES ============================================================================================
__________________________________________________________________________________ test_get_authentication_2 ___________________________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call -------------------------------------------------------------------------------------
Using interactive login to Azure. To use Service Principal authentication
====================================================================================== 1 passed in 0.02s =======================================================================================

Upvotes: 0

dumbledad
dumbledad

Reputation: 17555

It is because I was not specifying the patch correctly. Instead of using the import line as the template for my patch, I should have used the module path of the function being tested. In my case that means replacing this:

@patch("azureml.core.authentication.InteractiveLoginAuthentication")
def test_get_authentication(mocked_interactive_authentication: Any) -> None:
    util.get_authentication()
    assert mocked_interactive_authentication.called

with this:

@patch("health.azure.azure_util.InteractiveLoginAuthentication")
def test_get_authentication(mocked_interactive_authentication: Any) -> None:
    util.get_authentication()
    assert mocked_interactive_authentication.called

(I don't know how often I am going to make this mistake before it finally sinks in!)

Upvotes: 2

Related Questions