Properly mocking gcs Bucket and Blob in pytest MagicMock

276 views Asked by At

I'm trying to write test for this function. I'd like not to rely on real gcs object, but mock the objects.

# gcs_blobs.py file
from google.cloud import storage
        
    def check_existing_blobs(new_blob_names: list[str], bucket: storage.Bucket) -> list[storage.Blob]:
        # Check if any of the new blobs exists already on bucket
        current_blobs = [blob for blob in bucket.list_blobs()]
        existing_blobs = []
        for blob_name in new_blob_names:
            for blob in current_blobs:
                if blob_name == blob.name:
                    existing_blobs.append(blob)
        return existing_blobs

As I understand correctly, I should

  • mock Bucket.list_blobs() method so that it would return list of mocked Blob objects
  • mock Blob instance 'name' attribute so that it would return name I want

I'm trying to do that with no success. In test code below, check_existing_blobs(new_blob_names, bucket) returns empty list. In this case I'd like it to return blob with name blob1. How do I do that properly?

Would using 'side_effect' on blob.name allow me to return names from the provided 'existing_blob_names' list in consecutive calls to blob.name?

How to do this assertion properly, with list of blobs with names from the list?

# test_blobs.py file
from unittest.mock import Mock, MagicMock

import pytest
from google.cloud import storage

from .gcs_files import (
    check_existing_blobs
)


@pytest.mark.parametrize(
    "new_blob_names,existing_blob_names",
    [
        (
            ['blob1'],
            ['blob1', 'blob2'],
        ),
    ]
)
def test_contains_existing_files(mocker, new_blob_names, existing_blob_names):
    
    storage.Bucket = MagicMock()
    bucket = storage.Bucket()

    storage.Blob = MagicMock()
    blob = storage.Blob()
    
    #mocker.patch.object(blob, "name", new="blob_name") # after that blob.name prints "blob_name"
    mocker.patch.object(blob, "name", side_effect=existing_blob_names)

    print(blob.name) # output: <MagicMock name='name' id='139862698975120'>

    # in place of '???' it should be mock of storage.Blob instance which .name returns one of the blob names
    # assert check_existing_blobs(new_blob_names, bucket) == [???] 


Also please let me know if I should split this question to subproblems.

1

There are 1 answers

3
Kale Kundert On BEST ANSWER

I'd recommend not using MagicMock() for this. Instead, just make a new class that has the behavior you want:

class MockBucket:

    def __init__(self, names):
        self.blobs = [MockBlob(x) for x in names]

    def list_blobs(self):
        return self.blobs

class MockBlob:

    def __init__(self, name):
        self.name = name

I'd also recommend not assigning to storage.Bucket or storage.Blob. These assignments won't be reverted when the test function ends, so they have great potential to interfere with later tests. In fact, the purpose of mocker.patch and mocker.patch.object is to avoid this kind of assignment. In your case, though, there's no reason to patch anything, because the code being tested doesn't rely on any global variables.