James C.
James C.

Reputation: 531

Mocking and patching keystoneclient in Python unittest

I am writing unittest for a class looks like below. I am trying to assert if logging is properly called with patch keystoneclient. The class looks like below. Problem is, I cannot pass through the for statement and can never get to LOGGER.warning or LOGGER.info even after patching CredentialManager. I am new to whole unittest and Mock so I might not be understanding something clearly.

from keystoneclient.v3.client import Client
from keystoneclient.v3.credentials import CredentialManager
import logging

LOGGER = logging.getLogger(__name__)

class MyClass(object):

    def __init__(self):
    ...

    def myfunc(self):

        new_credentials = {}
        client = Client(
            username=self.username,
            password=self.password,
            auth_url=self.auth_url,
            user_domain_name=self.user_domain_name,
            domain_name=self.domain_name
        )

        abc = CredentialManager(client)

        for credential in abc.list():

            defg = str(credential.type)
            (access, secret) = _anotherfunc(credential.blob)

            if not defg:
                LOGGER.warning('no abc')

            if defg in new_credentials:
                LOGGER.info('Ignoring duplate')

            new_credentials[defg] = (access, secret)

My unit tests looks something like this,

import unittest
from mock import patch, MagicMock
import MyClass

LOGGER = logging.getLogger('my_module')

@patch('MyClass.Client', autospec = True)
@patch('MyClass.CredentialManager', autospec = True)
class TestMyClass(unittest.TestCase):

    def test_logger_warning(self,,mock_client, mock_cm):
        with patch.object(LOGGER, 'warning') as mock_warning:
            obj = MyClass()
            mock_warning.assert_called_with('no abc')

The error I am getting looks like this.

    for credential in abc.list():
AttributeError: 'tuple' object has no attribute 'list'

So even after patching CredentialManager with autospect, I am getting error on abc.list(). I need to get to the point where I can test LOGGER but it seems like this patching does not work for list(). How should I make this error go away and be able to pass through the for statment so I can assert on logging?

Upvotes: 0

Views: 394

Answers (1)

Michele d'Amico
Michele d'Amico

Reputation: 23711

How many details to cover in this answer:

  1. Patch order: @patch decorator are applied as stack, that means first decorator -> last mock argument
  2. If you want that CredentialManager().list() return something that contains a empty type you should instrument your mock_cm to do it
  3. You should call obj.myfunc() if you want to test something :)

The code:

import unittest from mock import patch, Mock import MyClass

@patch('MyClass.Client', autospec=True)
@patch('MyClass.CredentialManager', autospec=True)
class TestMyClass(unittest.TestCase):
    #Use decorator for test case too... is simpler and neat
    @patch("MyClass.LOGGER", autospec=True)
    def test_logger_warning(self, mock_logger, mock_cm, mock_client):
        #pay attention to the mock argument order (first local and the the stack of patches
        # Extract the mock that will represent you abc
        mock_abc = mock_cm.return_value
        # Build a mock for credential with desidered empty type
        mock_credential = Mock(type="")
        # Intrument list() method to return your credential
        mock_abc.list.return_value = [mock_credential]
        obj = MyClass.MyClass()
        # Call the method
        obj.myfunc()
        # Check your logger
        mock_logger.warning.assert_called_with('no abc')

Good point to use autospec=True for all your patches: it is a good practice.

Anyway I would like encourage you to extract your logging section to a method and test it by some fake credentials: is simpler and a better design: something like

def myfunc(self):

    new_credentials = {}
    client = Client(
        username=self.username,
        password=self.password,
        auth_url=self.auth_url,
        user_domain_name=self.user_domain_name,
        domain_name=self.domain_name
    )

    abc = CredentialManager(client)

    for credential in abc.list():
        self.process_credential(credential, new_credentials)

@staticmethod
def process_credential(credential, cache):
    (access, secret) = _anotherfunc(credential.blob)
    defg = str(credential.type)
    MyClass.logging_credential_process(credential, not defg, defg in cache)
    cache[defg] = (access, secret)

@staticmethod
def logging_credential_process(credential, void_type, duplicate):
    if void_type:
        LOGGER.warning('no abc')
    if duplicate:
        LOGGER.info('Ignoring duplate')

is simpler to test and looks better.

Upvotes: 1

Related Questions