spyoungtech/pyclip

what is the recommended way of testing pyclip?

Closed this issue · 4 comments

I've recently added pyclip to a cli app, and would love to write a unit test to check that it's called when I expect. I couldn't find anything in the docs or the source code that would let me register a mock clipboard to inspect during pytest testing.

I agree with you

pyclip has a unittest covering the core features.

Lacks:

  • separate module and unittest which deals solely with usage / integration

  • a testable module to turn pyclip into an optional feature.

Calling, for example, pyclip.copy wrapper should do nothing if pyclip or the dependency are not installed. Assume nothing!

  • a unittest that covers pyclip as an optional feature integrated into an UI app

Can answer both how to make pyclip an optional feature and how to unittest it at the same time. And i've done it, so drawing snippets from working code base.

urpackage.ui.util_screen.py (aka one of many possible screen features. pyclip being just one screen feature)

# Optional import
try:
    import pyclip
    is_pyclip_import = True
except (ImportError, pyclip.base.ClipboardSetupException):  # pragma: no cover
    is_pyclip_import = False

If import pyclip fails, take note, hidden within pyclip codebase, will check whether the dependency is installed. So there are two things occurring here. Which is inconvenient, but unavoidable.

In the unittest, both is_pyclip_import and pyclip can be patched, to pretend failures occurred.

urpackage.ui.util_screen.py

import sys
is_pyclip = "pyclip" in sys.modules

is_pyclip indicates pyclip import and dependency exists; both were successful. Module level variable so can be patched during test.

def _is_clipboard() -> bool:
    if not is_pyclip or not is_pyclip_import:
        ret = False
    else:
        # distro package dependency is installed
        try:
            pyclip.util.detect_clipboard()
        except (
            pyclip.base.ClipboardSetupException,
        ):  # pragma: no cover already checked for
            ret = False
        else:
            ret = True

    return ret

Also patchable. Check for both situations: pyclip not installed and dependency not installed.

Possible to test why it failed. But within an UI app, we actually don't care why. We care whether the pyclip command will work or not.

import os
is_wayland = os.environ.get("WAYLAND_DISPLAY", "") != ""

Pulling this code out as a patchable module level variable.

Next is one wrapper function per pyclip command. The wrapper function should log issues and display passive notification to user, indicating what the problem is and how to fix the issue. Not just what the problem is.

Each wrapper function can return a boolean. Either the command was successful or it wasn't

Unittest

The unittest would be UI package specific. Will show all the patch blocks, so irregardless of UI framework can build the unittests!

import unittest
from unittest.mock import patch
from urpackage.ui.util_screen import (
    _is_clipboard,
    is_pyclip,
)

class ImportPyclipFail(unittest.TestCase):
    @unittest.skipIf(
        is_clipboard is False,
        "",
    )
    def test_works_or_doesnt(self):
        """Check _is_clipboard works irregardless of actual situation"""
        with (
            patch("urpackage.ui.util_screen.is_pyclip", False)
        ):
            self.assertFalse(_is_clipboard())

        with patch(
            "urpackage.ui.util_screen.pyclip.util.detect_clipboard",
            return_value=True,
        ):
            self.assertTrue(_is_clipboard())

        """Can't patch "urpackage.ui.util_screen.pyclip.util.detect_clipboard" 
        affects import pyclip
        """
        pass

async tests patches


with (
    patch("urpackage.ui.util_screen.is_pyclip", False),
    patch("urpackage.ui.util_screen._is_clipboard", return_value=False),
):

@unittest.skipIf(
    is_clipboard is True,
    "",
)

import os
from ur_ui_framework import Label
gui_component = Label()
with patch("os.environ.get", return_value="1"):
    self.assertFalse(copy_result(gui_component))
#    xclip
with patch("os.environ.get", return_value=""):
    self.assertFalse(copy_result(gui_component))

^^ is not the complete unittest, but has all the patches, so anything else, filling in the UI framework specific bits, should not be in the least bit challenging.

Finally

if __name__ == "__main__":  # pragma: no cover
    unittest.main(tb_locals=True)

Then run your test, from package base folder, python -m tests.ui.test_util_screen

unittest was used rather than pytest cuz pytest is an unnecessary dependency. There are no important pytest features present in the existing pyclip tests.

@spyoungtech Please let me know whether or not you'd accept as an optional feature module

Cuz you haven't yet accepted the pull request, #25

so i'm reluctant to create a pull request until #25 is accepted.

write a unit test to check that it's called when I expect

I'm not sure what difficulty you're having or what interface you'd expect to be provided specifically, but for testing your own code, it is possible to test with mocks, just like you might mock out any component of any library.

Suppose you have a module like something.py that you expect to call pyclip.copy.

# something.py
import pyclip

def something(foo: str):
    pyclip.copy(foo)

Then in a test, you can use a mock pyclip.copy and assert that it has been called.

from unittest import mock
from something import something

def test_something():
    with mock.patch('pyclip.copy') as mock_copy:
        something('hello world')
        mock_copy.assert_called_with('hello world')

You can make the mock wrap the real copy function, too. This will pass the call to the mocked object and allow the clipboard contents to actually change while capturing all calls in the magic mock object. You can also directly use the functions of the library to inspect/assert the contents of the clipboard in your tests:

from unittest import mock
from something import something
import pyclip

def test_something():
    with mock.patch('pyclip.copy', wraps=pyclip.copy) as mock_copy:
        something('hello world')
        mock_copy.assert_called_with('hello world')
    assert pyclip.paste(text=True) == 'hello world'

The unittest.mock docs should be helpful for other uses.

Seems like you've got what you need. Feel free to open another issue if you need any more help.