0

I am new to pytest. I am trying to mock/replace my client.py with fake_client.py for the testing. The fake_client class contains the same methods as the client class.

Here is my project structure and code:- abc/base_class.py

from .client import Client

class PushBase:
    def __init__(self, hostname):
        self.client = Client(hostname)

    def process_product(self, item): # item type is dict {}
        product_id = item.get('product_id')

        if item.get('state') == 'present':
            if not product_id:
                # creating product
                product_id = self.client.create_product(item.get('data'))
                return product_id
            
            # update product
            self.client.update_product(item.get('data'))
        elif item.get('state') == 'absent':
            # delete product
            self.client.delete_product(product_id)

This is my client.py with API calls in abc/client.py

class Client:
    def __init__(self, hostname):
        self.hostname = hostname
        # some other stuff

    def create_product(self, params=None):
        # some code
    def update_product(self, params=None):
         # some code
    def delete_product(self, params=None):
         # some code

I have created a fake client to test against the actual client.py and it has the same methods as the client.py in tests/fake_client.py

class FakeClient:
    def __init__(self, *args, **kwargs):
        pass

    def create_product(self):
        # some code
    def update_product(self):
         # some code
    def delete_product(self):
         # some code

in tests/test_base_class.py

from tests.fake_client import FakeClient
import unittest
from abc.base_class import BaseClass
import pytest
try:
    import mock
except ImportError:
    from unittest import mock


class TestBaseClassOperations(unittest.TestCase):
    def setUp(self):
        self.push_base = BaseClass("http://fake_host_nmae/test", "foo", "bar")
        self.push_base.client = mock.patch('abc.base_class.Client', new=FakeClient()).start()

    def test_create_valid_product(self):
        product_dict = { # some stuff }
        created_product_id = self.push_base.process_product(product_dict)
        # process_product() will call create_product from fake client
        # will return 1 if FakeClient().create_product() called
        assert created_product_id == 1

I tried it another way.

@pytest.fixture
def fixture_product_creation():
    return { # product fixture
    }
    
@mock.patch('abc.base_class.Client', return_value=FakeClient())
class TestBaseClassAgain:
    def test_create_valid_product(self, mock_client, fixture_product_creation):
        push_base = BaseClass("http://fake_host_nmae/test", "foo", "bar")
        
        created_product_id = push_base.process_product(fixture_product_creation)
        expected = 1
        assert created_product_id == expected
        # how can I use this mock_client here?

Although I can replace the client with the FakeClient, but I am unsure how to arrange all the mock things to get it tested with the assert or assert_called_with calls.

I referred this but not able to arrange it in a proper pythonic way. Can anyone help me rearrange this and suggest to me any better way to replace the client class with the fake client class by using pytest mock?

Thanks.

Javed
  • 1,613
  • 17
  • 16

1 Answers1

1

In order to properly test this you should make a change to your PushBase class. It is not good practice to instantiate a dependent object in the __init__ method of your class, instead consider passing the object in. This makes testing easier as you can just inject the dependency as needed. Another option would be to make a @classmethod that instantiates the object with the client. In the code below I illustrate how to do the former.

It also appears you have an indentation error as the update_product method can never be called based on the logic you currently have.

# base.py

class PushBase:
    def __init__(self, client):
        self.client = client

    def process_product(self, item): # item type is dict {}
        product_id = item.get('product_id')

        if item.get('state') == 'present':
            if not product_id:
                # creating product
                product_id = self.client.create_product(item.get('data'))
                return product_id
            
            # update product
            self.client.update_product(item.get('data'))
        elif item.get('state') == 'absent':
            # delete product
            self.client.delete_product(product_id)


# test_base.py

import pytest

from src.base import PushBase



def test_create_product_id(mocker):
    mock_client = mocker.MagicMock()
    base = PushBase(mock_client)

    item = {
        "state": "present",
        "data": "fizz"
    }

    mock_client.create_product.return_value = "ok"

    product_id = base.process_product(item)

    assert product_id == "ok"
    mock_client.create_product.assert_called_once_with("fizz")


def test_update_product(mocker):
    mock_client = mocker.MagicMock()
    base = PushBase(mock_client)

    item = {
        "state": "present",
        "data": "bang",
        "product_id": "baz"
    }

    base.process_product(item)
    mock_client.update_product.assert_called_once_with("bang")


def test_delete_product(mocker):
    mock_client = mocker.MagicMock()
    base = PushBase(mock_client)

    item = {
        "state": "absent",
        "product_id": "vroom"
    }

    base.process_product(item)
    mock_client.delete_product.assert_called_once_with("vroom")

============================================== test session starts ===============================================
platform darwin -- Python 3.8.9, pytest-7.0.1, pluggy-1.0.0
rootdir: ***
plugins: asyncio-0.18.3, hypothesis-6.48.1, mock-3.7.0
asyncio: mode=strict
collected 3 items                                                                                                

tests/test_base.py ...

=============================================== 3 passed in 0.01s ================================================

I am using the pytest-mock package, which is where the mocker fixture comes from. The nice thing about being able to inject a dependency into your class is you don't need to configure all the methods beforehand, you can modify what you need within each test function. There are improvements you can make to the tests above, but that exercise is left to you. Hopefully this should help you understand the direction you should go in.

gold_cy
  • 13,648
  • 3
  • 23
  • 45
  • In this case, the above code will not test the tests/fake_client.py. It will simply mock the client methods in base_class. And I'm instantiate the objects in __init__ because it has many other dependencies(didn't add in the above code to keep my question simple) and some other dependent object calls which would be difficult to maintain if I inherit it. So the dependency injection may not be suitable in my case. I found a workaround and wrote some customer classes in the test_client class to achieve the same. And good catch for the update method call I have updated it (was a copy-paste error). – Javed Jul 12 '22 at 15:05
  • based on how you phrased the problem, it looks like you are trying to test the base class. if you want to test the client then you shouldn’t be doing that from inside the base class, that is not how unit testing works. the reason I mocked the client is because these tests test the functionality of the base class. also my suggestion is not to inherit it, but to pass it in, inheritance is something completely different. – gold_cy Jul 12 '22 at 15:33