0

To be specific, I'm using the python O365 library in a custom SharePoint repository. It heavily relies on some classes from this library and I need to mock out functionalities since I can't just connect to any real resources in my tests.

Here is the key part of my repository.

class SharepointRepository():
    def __init__(self, app=None):
        self._account = Account(
          ...
        ) # O365.account.Account

    sites = self._account.sharepoint().search_site(app.config.get("SP_SITE"))
    self._site = sites[0] # O365.sharepoint.Site

    def _get_drive_by_name(self, drive_name):
        for drive in self._site.list_document_libraries():
            if drive.name == drive_name:
                return drive

    def delete_file(self, drive, path):
        item = drive.get_item_by_path(path) # O365.drive.Drive
        if not item.delete(): # O365.drive.DriveItem
            raise Exception( 

I've tried to monkeypatch the called methods from the library one by one, but the problem is that these attribute objects are created by the library itself and would already make real connections. So I can't just create or inject them myself.

import pytest
from O365.account import Account
from O365.sharepoint import Site
from O365.drive import Drive

class TestSharePoint:
    @pytest.fixture(autouse=True)
    def setup(self):
        self.client = SharepointRepository()
        self.drive_name = "test drive"
        self.path = "test path"
        self._account = Account() # This does not work, I need to mock this.
        self._site = Site() # This does not work, I need to mock this.

    def test_sharepoint_file_delete(self, monkeypatch):
        def get_drive(drive_name):
            assert drive_name == self.drive_name

        def get_item(drive, path):
            assert drive == self.drive_name
            assert path == self.path

        with NamedTemporaryFile() as file:
            monkeypatch.setattr(Site, "list_document_libraries", get_drive)
            monkeypatch.setattr(Drive, "get_item_by_path", get_item)

            self.client.delete_file(file, self.drive_name, self.path)

How can I mock these library classes with their methods instead of trying to patch every single method this way?

MattSom
  • 2,097
  • 5
  • 31
  • 53
  • If you inverted the dependency it would be easier to test - `SharepointRepository` shouldn't _create_ an instance of `O365.account.Account`, for example, it should be _given_ one. – jonrsharpe Jun 14 '21 at 14:23
  • I can't give one either. It is created from complex credentials and it instantly connects to an O365 account. Which I can't provide for testing. – MattSom Jun 14 '21 at 14:24
  • Yes, but if the instance is being passed into `__init__` rather than created within it you can provide a _test double_ in the tests, not a real instance. You can also create your own facade around the interfaces you don't own, and inject a test double of that for testing. – jonrsharpe Jun 14 '21 at 14:25
  • I can't break this pattern unfortunately, because I have to follow an interface implementation. I just left that out for simplicity from the class header. – MattSom Jun 14 '21 at 14:28
  • I'm not sure what you mean by *"I have to follow an interface implementation"*. – jonrsharpe Jun 14 '21 at 14:29
  • I mean this is just one repository implementation for O365 Sharepoint and I have an interface for many different repositories which won't have any `Account` around, only the `app`. – MattSom Jun 14 '21 at 14:32
  • I'm not sure why that's a problem, `SharepointRepository` can still implement whatever that interface is. If there's other context that will impact solutions, [edit] the question to provide it. – jonrsharpe Jun 14 '21 at 14:34
  • I won't have an `Account` where I'm going to use this. That layer has nothing to do with any of the actual implementation in use - provided by config vars. But I'd like to ask: that test double would be also a mock isn't it? – MattSom Jun 14 '21 at 14:39
  • 1
    Then maybe there's a class method e.g. `from_app` that creates the instances of those other dependencies and makes an instance of the repository, but from what you've posted we can't see what if anything `Account` even needs. A mock is a type of test double, see e.g. https://tanzu.vmware.com/content/pivotal-engineering-journal/the-test-double-rule-of-thumb-2 – jonrsharpe Jun 14 '21 at 14:46
  • I did not want to dilute the question with that part, but yes: Every repository gets only the `app` which has the proper configs bound on it. This `Account` needs these configs as well, but that's the repo's job how it handles it. So I inject the data via the `app`. I'm reading (similar questions)(https://stackoverflow.com/a/39004889/6560773) now and trying to mock them the same way. – MattSom Jun 14 '21 at 15:00

0 Answers0