Account for warnings when testing; add new test fixtures
authorKarl O. Pinc <kop@karlpinc.com>
Sat, 16 Mar 2024 21:31:30 +0000 (16:31 -0500)
committerKarl O. Pinc <kop@karlpinc.com>
Sat, 16 Mar 2024 21:31:52 +0000 (16:31 -0500)
src/pgwui_develop/testing.py
tests/test_pgwui.py
tests/test_testing.py
tests/tested_module.py [new file with mode: 0644]
tox.ini

index 8c81abeb74334612301719f9e1de8d671331c0f7..bada279850bfc16e55763dcfc1acef6753d0928c 100644 (file)
 
 # Karl O. Pinc <kop@karlpinc.com>
 
-from pytest import fixture
 from unittest import mock
+import functools
+import sys
+import warnings
+import pytest
+
+
+# Utility decorator
+
+# Pyramid 2.0 added deprecation warnings to pyramid.request.Request.
+# These express when unittest.mock accesses the class' attributes
+# to construct a mock, when the mock_request_blank fixture is used
+# and a mock made.  Ignore all warnings triggered by unittest.mock.
+def ignore_deprecation_warnings(ignore):
+    def identity(func):
+        return func
+
+    def wrapper(func):
+        @functools.wraps(func)
+        def wrapped(*args, **kwargs):
+            with warnings.catch_warnings():
+                warnings.filterwarnings(
+                    "ignore", category=DeprecationWarning,
+                    module='unittest.mock')
+                return func(*args, **kwargs)
+        return wrapped
+
+    if ignore:
+        return wrapper
+    return identity
 
 
 # Mock support
 
-def make_mock_fixture(module, method, autouse=False, wraps=None):
+def make_mock_fixture(module, method,
+                      autouse=False, wraps=None, ignore_deprecation=True):
     '''Returns a pytest fixture that mocks a module's method or a class's
     class method.  "module" is a module or a class, but method is a string.
     '''
-    @fixture(autouse=autouse)
-    def fix(monkeypatch):
-        mocked = mock.Mock(
+    @ignore_deprecation_warnings(ignore_deprecation)
+    def make_mock():
+        return mock.Mock(
             spec=getattr(module, method), name=method, wraps=wraps)
+
+    @pytest.fixture(autouse=autouse)
+    def fix(monkeypatch):
+        mocked = make_mock()
         monkeypatch.setattr(module, method, mocked)
         return mocked
     return fix
 
 
-def make_magicmock_fixture(module, method, autouse=False, autospec=False):
+def make_magicmock_fixture(
+        module, method,
+        autouse=False, autospec=False, ignore_deprecation=True):
     '''Returns a pytest fixture that magic mocks a module's method or a
     class's class method.  "module" is a module or a class, but method
     is a string.
     '''
-    @fixture(autouse=autouse)
-    def fix(monkeypatch):
+    @ignore_deprecation_warnings(ignore_deprecation)
+    def make_mocked():
         if autospec:
-            mocked = mock.create_autospec(
+            return mock.create_autospec(
                 getattr(module, method), spec_set=True)
-        else:
-            mocked = mock.MagicMock(
-                spec=getattr(module, method), name=method)
+        return mock.MagicMock(
+            spec=getattr(module, method), name=method)
+
+    @pytest.fixture(autouse=autouse)
+    def fix(monkeypatch):
+        mocked = make_mocked()
         monkeypatch.setattr(module, method, mocked)
         return mocked
     return fix
 
 
+def function_mock_fixture(method, ignore_deprecation=True):
+    '''Returns a pytest fixture that mocks a function.
+    "method" is the actual function.
+    The primary purpose is to monkeypatch modules
+    that are needed by python's mechanics.
+    '''
+    name = method.__name__
+    module = sys.modules[method.__module__]
+
+    @ignore_deprecation_warnings(ignore_deprecation)
+    def make_mocked():
+        return mock.Mock(spec=module, name=name)
+
+    @pytest.fixture
+    def fix(monkeypatch):
+        def run():
+            mocked = make_mocked()
+            func = getattr(mocked, name)
+            monkeypatch.setattr(module, name, func)
+            return func
+        return run
+    return fix
+
+
+def instance_mock_fixture(method, ignore_deprecation=True):
+    '''Returns a pytest fixture that mocks a instance method.
+    "method" is the actual instance or class method.
+    The primary purpose is to monkeypatch classes
+    that are needed by python's mechanics.
+    '''
+    name = method.__name__
+    cls = method.__self__
+
+    @ignore_deprecation_warnings(ignore_deprecation)
+    def make_mocked():
+        return mock.Mock(spec=cls, name=name)
+
+    @pytest.fixture
+    def fix(monkeypatch):
+        def run():
+            mocked = make_mocked()
+            func = getattr(mocked, name)
+            monkeypatch.setattr(cls, name, func)
+            return func
+        return run
+    return fix
+
+
 def late_instance_mock_fixture(method, ignore_deprecation=True):
     '''Returns a pytest fixture that mocks a instance method.
     "method" is the name of the instance or class method.
     The function returned by the fixture takes the class instance to
     be monkeypatched.
 
-    The fixture is called by the test function with the class instance
-    that's to be monkeypatched and the mock is returned for the
-    test function to configure/etc.
+    Useful to monkeypatch classes produced by fixtures.
     '''
-    @fixture
+    @ignore_deprecation_warnings(ignore_deprecation)
+    def make_mocked(cls):
+        return mock.Mock(spec=cls, name=method)
+
+    @pytest.fixture
     def fix(monkeypatch):
         def run(cls):
-            mocked = mock.Mock(spec=getattr(cls, method), name=method)
-            monkeypatch.setattr(cls, method, mocked)
-            return mocked
+            mocked = make_mocked(cls)
+            func = getattr(mocked, method)
+            monkeypatch.setattr(cls, method, func)
+            return func
         return run
     return fix
index 1807efe8639ff12cd9e45e68cb6b30085869361b..b4f2edd1925f02a1ef74f4cd7d31c0ef1f8a6f4e 100644 (file)
@@ -1,4 +1,5 @@
-# Copyright (C) 2020, 2021 The Meme Factory, Inc.  http://www.karlpinc.com/
+# Copyright (C) 2020, 2021, 2024 The Meme Factory, Inc.
+# http://www.karlpinc.com/
 
 # This file is part of PGWUI_Develop.
 #
@@ -35,12 +36,12 @@ mock_text_error_template = testing.make_magicmock_fixture(
     pgwui.mako.exceptions, 'text_error_template')
 MockTemporaryDirectory = testing.make_magicmock_fixture(
     pgwui.tempfile, 'TemporaryDirectory')
-mock_set_extraction_path = testing.instance_method_mock_fixture(
-    'set_extraction_path')
-mock_resource_filename = testing.instance_method_mock_fixture(
-    'resource_filename')
-mock_cleanup_resources = testing.instance_method_mock_fixture(
-    'cleanup_resources')
+mock_set_extraction_path = testing.function_mock_fixture(
+    pgwui.pkg_resources.set_extraction_path)
+mock_resource_filename = testing.function_mock_fixture(
+    pgwui.pkg_resources.resource_filename)
+mock_cleanup_resources = testing.function_mock_fixture(
+    pgwui.pkg_resources.cleanup_resources)
 mock_scandir = testing.make_magicmock_fixture(
     pgwui.os, 'scandir')
 
@@ -376,9 +377,9 @@ def test_deliver_target(
         mock_cleanup_resources):
     '''All the mocks are called
     '''
-    mocked_set_extraction_path = mock_set_extraction_path(pgwui.pkg_resources)
-    mocked_resource_filename = mock_resource_filename(pgwui.pkg_resources)
-    mocked_cleanup_resources = mock_cleanup_resources(pgwui.pkg_resources)
+    mocked_set_extraction_path = mock_set_extraction_path()
+    mocked_resource_filename = mock_resource_filename()
+    mocked_cleanup_resources = mock_cleanup_resources()
 
     pgwui.deliver_target(None, None)
 
index a72458303ec8fc883428b22127ca4d669fe644de..7680763d3d94acd6bbbf1e3b0d3ef92b7c961fc6 100644 (file)
 
 
 import pytest
-
 import sys
 
+import tested_module
 from pgwui_develop import testing
 
 
 # Test functions
 
+#
+# ignore_depreciation_warnings()
+#
+
+
+@pytest.mark.parametrize(
+    ('ignore', 'warn_cnt'),
+    [pytest.param(
+        'True', 0,
+        marks=pytest.mark.xfail(
+            reason="For reasons unknown, pytester reports 1 warnings")),
+     ('False', 1)])
+@pytest.mark.integrationtest
+@pytest.mark.unittest
+def test_ignore_deprecation_warnings_ignore(pytester, ignore, warn_cnt):
+    '''The expected number of deprecation warnings are raised when warnings
+    are ignored, or not
+    '''
+
+    pytester.makepyfile(
+        f"""
+    import pytest
+    import warnings
+    from unittest import mock
+    from pgwui_develop import testing
+
+    # Class which raises a deprecation warning
+    class WithDeprecation():
+        '''Test class that raises a DeprecationWarning when mocked
+        '''
+        def __getattribute__(self, name):
+            if name == 'oldmethod':
+                warnings.warn('oldmethod is deprecated',
+                              category=DeprecationWarning)
+            return super().__getattribute__(name)
+
+        def oldmethod(self):
+            return "I am old, so old."
+
+    mocked_oldmethod = testing.make_mock_fixture(
+        WithDeprecation, 'oldmethod',
+        ignore_deprecation={ignore})
+
+    def test_ignore_deprecation_warnings_ignore(mocked_oldmethod):
+        expected = 'something'
+        mocked_oldmethod.return_value = expected
+        assert WithDeprecation().oldmethod() == expected
+    """)
+
+    result = pytester.runpytest()
+
+    result.assert_outcomes(passed=1, warnings=warn_cnt)
+
+
 #
 # make_mock_fixture()
 #
@@ -94,9 +148,38 @@ def test_make_magicmock_fixture_autospec(magic_mocked_autospecced_func):
 
 
 #
-# instance_method_mock_fixture()
+# function_mock_fixture()
 #
 
+f_mocked_method = testing.function_mock_fixture(tested_module.method_to_mock)
+
+
+@pytest.mark.unittest
+@pytest.mark.integrationtest
+def test_function_mock_fixture(f_mocked_method):
+    # The mock of the instance method works
+
+    test_value = 'mocked value'
+    f_mocked_method().return_value = test_value
+
+    result = tested_module.method_to_mock()
+
+    assert result == test_value
+
+
+@pytest.mark.unittest
+@pytest.mark.integrationtest
+def test_function_mock_fixture_unmocked():
+    # The test function works after the mocking
+
+    result = tested_module.method_to_mock()
+
+    assert result == tested_module.normal_return_value
+
+
+#
+# Setup for mocking instance methods
+#
 normal_return_value = 'not mocked'
 
 
@@ -107,7 +190,36 @@ class TestClass():
         return normal_return_value
 
 
-mocked_method = testing.instance_method_mock_fixture('method_to_mock')
+test_instance = TestClass()
+
+
+#
+# instance_mock_fixture()
+#
+i_mocked_method = testing.instance_mock_fixture(test_instance.method_to_mock)
+
+
+@pytest.mark.unittest
+@pytest.mark.integrationtest
+def test_instance_mock_fixture(i_mocked_method):
+    # The mock of the instance method works
+
+    test_value = 'mocked value'
+    i_mocked_method().return_value = test_value
+
+    result = test_instance.method_to_mock()
+
+    assert result == test_value
+
+
+@pytest.mark.unittest
+@pytest.mark.integrationtest
+def test_instance_mock_fixture_unmocked():
+    # The test function works after the mocking
+
+    result = test_instance.method_to_mock()
+
+    assert result == normal_return_value
 
 
 #
diff --git a/tests/tested_module.py b/tests/tested_module.py
new file mode 100644 (file)
index 0000000..59004ab
--- /dev/null
@@ -0,0 +1,27 @@
+# Copyright (C) 2024 The Meme Factory, Inc.
+# http://www.karlpinc.com/
+
+# This file is part of PGWUI_Develop.
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public
+# License along with this program.  If not, see
+# <http://www.gnu.org/licenses/>.
+#
+
+# Karl O. Pinc <kop@karlpinc.com>
+
+normal_return_value = 'not mocked'
+
+
+def method_to_mock():
+    return normal_return_value
diff --git a/tox.ini b/tox.ini
index aca3ac0b42c844d2c39e151ab80bb937921facb6..eb2c6543ddf64be2b6f855cf5e6500fb4802889d 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -49,3 +49,5 @@ addopts = --cov --cov-append
 markers =
   unittest
   integrationtest
+pytest_plugins =
+  pytester