3

I've read a few tutorials on mocking in Python, but I'm still struggling :-/

For example, I have a function wrapping a call to google storage to write a blob.

I'd like to mock the google.storage.Client().bucket(bucket_name) method to return an exceptions.NotFound for a specific non-existent bucket. I'm using side_effect to set the excepted exception

Do you know what I'm doing wrong?

Below is what I tried (I'm using 2 files: main2.py and main2_test.py):

# main2.py
import logging
from google.cloud import storage

def _write_content(bucket_name, blob_name, content):
   storage_client = storage.Client()
   bucket = storage_client.bucket(bucket_name)
   blob = bucket.blob(blob_name)

   try:
        blob.upload_from_string(data=content)
        return True
    except Exception:
        logging.error("Failed to upload blob")
        raise

and

# main2_test.py
import pytest
from unittest.mock import patch
from google.api_core import exceptions
import main2


@patch("main2.storage.Client", autospec=True)
def test_write_content(clientMock):
    bucket_name = "not_existent_bucket"
    clientMock().bucket(bucket_name).side_effect = exceptions.NotFound

    with pytest.raises(exceptions.NotFound):
        main2._write_content(bucket_name, "a_blob_name", '{}')

Example call

pytest main2_test.py::test_write_content

Result

platform linux -- Python 3.7.7, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
rootdir: /home/user/project, inifile: pytest.ini
plugins: requests-mock-1.8.0
collected 1 item                                                                                                                                                                                     

main2_test.py::test_write_content FAILED                                                                                                                                                       [100%]

============================================================================================== FAILURES ==============================================================================================
_________________________________________________________________________________________ test_write_content _________________________________________________________________________________________

clientMock = <MagicMock name='Client' spec='Client' id='139881522497360'>

    @patch("main2.storage.Client", autospec=True)
    def test_write_content(clientMock):
        bucket_name = "my_bucket"
        clientMock().bucket(bucket_name).side_effect = exceptions.NotFound
    
        with pytest.raises(exceptions.NotFound):
>           main2._write_content(bucket_name, "a_blob_name", '{}')
E           Failed: DID NOT RAISE <class 'google.api_core.exceptions.NotFound'>

main2_test.py:14: Failed
=====================================
FAILED main2_test.py::test_write_content - Failed: DID NOT RAISE <class 'google.api_core.exceptions.NotFound'>
=====================================
MrBean Bremen
  • 14,916
  • 3
  • 26
  • 46
AxA
  • 319
  • 5
  • 18

1 Answers1

3

Your test has two problems: you are not mocking the method that shall actually raise (upload_from_string), and you are setting an exception class instead of an exception as side effect.

The following would work:

@patch("main2.storage.Client", autospec=True)
def test_write_content(clientMock):
    blob_mock = clientMock().bucket.return_value.blob.return_value  # split this up for readability
    blob_mock.upload_from_string.side_effect = exceptions.NotFound('testing')  # the exception is created here

    with pytest.raises(exceptions.NotFound):
        main2._write_content("not_existent", "a_blob_name", '{}')

Note also that setting a specific parameter for the bucket call has no effect, as it is called on a mock, and the argument is just ignored - I replaced it with return_value, which makes this clearer.

MrBean Bremen
  • 14,916
  • 3
  • 26
  • 46
  • Thanks a lot :-) Besides the 2 problems about what to mock and the exception returned, I was definitively having hard time to get the whole return_value chain (and when it applies for class VS instance methods). I had to re-read the article I linked a few more times. Plus, I'd have had to fight long before figuring out the fact that mock does not record the arguments. It seemed obvious to me that it would do that, but nope. – AxA Jul 10 '20 at 22:01
  • 2
    Note that the mock does _record_ the arguments, so you ask it with which arguments it has been called, it just cannot _use_ them in the way the original code does. You may also check [this blog post](https://nedbatchelder.com//blog/201908/why_your_mock_doesnt_work.html) by Ned Batchelder and [this cheatsheet](https://medium.com/@yeraydiazdiaz/what-the-mock-cheatsheet-mocking-in-python-6a71db997832) for mocking. – MrBean Bremen Jul 11 '20 at 14:06