Search code examples
pythonunit-testingpython-unittestpython-mock

How correctly mock class dependencies?


I've recently started learning how to use mock from unittest library and encountered a problem figuring out how to correctly mock class dependencies. Below is the example i'm trying to mock

client.py

class HttpClient:

    def request(self, method, url, params = None):
        if method == "GET":
            return requests.get(url)
        elif method == "POST":
            return requests.post(url, body=params)

Here I inject the HttpClient object into the Post class

As I understood I need to mock self.client.request and this can be replaces with requests get or maybe in some other way?

data.py

class Post:

    def __init__(self, client: HttpClient):
        self.client = client
        self.base_url = "https://jsonplaceholder.typicode.com"

    def get_posts(self, amount):
        response = self.client.request(method="GET", url=f"{self.base_url}/posts/{amount}")

        if response.ok:
            return response.json()

        return response.status_code

And now the testing part

test_data.py

class TestPost(unittest.TestCase):

    @patch('app.client.HttpClient')
    def setUp(self, module):
        self.mock_http = MagicMock(autospec=HttpClient)
        self.mock_post = Post(self.mock_http)

    @patch.object(requests, 'get')
    def test_get_posts(self, mock_data):
        mock_data.return_value = {
            'postId': 1,
            'title': 'My title',
            'description': 'Post description'
        }

        response = self.mock_post.get_posts(1)

        assert response['postId'] == 1

So does when I set mock_data.return_value it actually replace the original response that is being called here response = self.mock_post.get_posts(1) or not?

Maybe someone could explain how this exactly works

Thank You!


Solution

  • Since you use dependency injection, you don't need to use mock.patch(). Just create mocked HttpClient object and pass it to the Post class. If your module depends on some modules which imported by import keyword, then you need to use mock.patch() things to mock them.

    Besides, you can create mocked Response for the client.request() method. We can use the Response class from the requests package.

    data.py:

    from client import HttpClient
    
    
    class Post:
    
        def __init__(self, client: HttpClient):
            self.client = client
            self.base_url = "https://jsonplaceholder.typicode.com"
    
        def get_posts(self, amount):
            response = self.client.request(method="GET", url=f"{self.base_url}/posts/{amount}")
    
            if response.ok:
                return response.json()
    
            return response.status_code
    

    test_data.py:

    import unittest
    from requests import Response
    from unittest.mock import MagicMock, Mock
    from client import HttpClient
    from data import Post
    
    
    class TestPost(unittest.TestCase):
    
        def setUp(self):
            self.mock_http = MagicMock(autospec=HttpClient)
            self.mock_post = Post(self.mock_http)
    
        def test_get_posts(self):
    
            mock_response = Mock(spec=Response)
            mock_response.json.return_value = {
                'postId': 1,
                'title': 'My title',
                'description': 'Post description'
            }
            self.mock_http.request.return_value = mock_response
    
            response = self.mock_post.get_posts(1)
            self.mock_http.request.assert_called_once_with(method="GET", url="https://jsonplaceholder.typicode.com/posts/1")
            assert response['postId'] == 1
    
    
    if __name__ == '__main__':
        unittest.main(verbosity=2)
    

    test result:

    test_get_posts (__main__.TestPost) ... ok
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.002s
    
    OK
    Name                                      Stmts   Miss  Cover   Missing
    -----------------------------------------------------------------------
    src/stackoverflow/69751753/client.py          7      4    43%   6-9
    src/stackoverflow/69751753/data.py           10      1    90%   16
    src/stackoverflow/69751753/test_data.py      18      0   100%
    -----------------------------------------------------------------------
    TOTAL                                        35      5    86%