vutils-testing


Namevutils-testing JSON
Version 1.0.3 PyPI version JSON
download
home_pagehttps://github.com/i386x/vutils-testing
SummaryAuxiliary library for writing tests
upload_time2023-10-15 17:46:04
maintainer
docs_urlNone
authorJiří Kučera
requires_python<4,>=3.7
licenseMIT
keywords testing mocking unit testing
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            [![Coverage Status](https://coveralls.io/repos/github/i386x/vutils-testing/badge.svg?branch=main)](https://coveralls.io/github/i386x/vutils-testing?branch=main)
![CodeQL](https://github.com/i386x/vutils-testing/actions/workflows/codeql.yml/badge.svg)

# vutils-testing: Auxiliary Library for Writing Tests

This package provides a set of tools that help with writing tests. It helps
with creating test data and types, mocking objects, patching, and verifying
test results.

## Installation

To install the package, type
```sh
$ pip install vutils-testing
```

## How to Use

For more details, please follow the subsections below.

### Type Factories

Sometimes tests require new types to be defined. To do this,
`vutils.testing.utils` provides `make_type` function, which is a wrapper of
`type`:
```python
# Create type derived directly from object:
my_type = make_type("MyType")

# Create class derived directly from Exception:
my_error = make_type("MyError", Exception)

# Create class derived from A and B:
my_class = make_type("MyClass", (A, B))

# Create class derived from A with foo member:
my_another_class = make_type("MyAnotherClass", A, {"foo": 42})

# Create class derived from object with foo member:
my_test_class = make_type("MyTestClass", members={"foo": 42})

# Key-value arguments other than bases and members are passed to
# __init_subclass__:
my_fourth_class = make_type("MyFourthClass", bases=A, foo=42)
```

### Mocking Objects and Patching

`make_mock`, `make_callable`, and `PatcherFactory` from `vutils.testing.mock`
allow to create mock objects and patching things.

`make_mock(*args, **kwargs)` is a shortcut for `unittest.mock.Mock`

`make_callable(x)` creates also instance of `unittest.mock.Mock`, but it
specifies its function-related behavior: if `x` is callable, it is used to do a
side-effect, otherwise it is used as the return value.
```python
# func_a() returns 3
func_a = make_callable(3)

container = []

# func_b() appends 42 to container
func_b = make_callable(lambda *x, **y: container.append(42))

# func_c() returns func_b
func_c = make_callable(lambda *x, **y: func_b)
```

`PatcherFactory` allows to use `unittest.mock.patch` multiple-times without
need of nested `with` statements. When instantiated, `setup` method is called.
`setup` method, implemented in the subclass, then may define set of patcher
specifications via `add_spec` method:
```python
class MyPatcher(PatcherFactory):

    @staticmethod
    def setup_foo(mock):
        mock.foo = "foo"

    @staticmethod
    def setup_baz(baz):
        baz["quux"] = 42

    def setup(self):
        self.baz = {}
        # When self.patch() is called:
        # - create a mock object, apply setup_foo on it, and patch foopkg.foo
        #   with it:
        self.add_spec("foopkg.foo", self.setup_foo)
        # - patch foopkg.bar with 42:
        self.add_spec("foopkg.bar", new=42)
        # - apply setup_baz on baz and patch foopkg.baz with it (create=True
        #   and other possible key-value arguments are passed to
        #   unittest.mock.patch):
        self.add_spec("foopkg.baz", self.setup_baz, new=self.baz, create=True)

patcher = MyPatcher()

with patcher.patch():
   # Patches are applied in order as specified by add_spec and reverted in
   # reverse order.
   do_something()
```

### Covering `mypy` Specific Code

When a module contains code that is visible only to `mypy`, it is not executed
during unit tests and hence reported as not covered. Function `cover_typing`
from `vutils.testing.utils` module has the ability to execute such a code and
therefore improve coverage reports:
```python
# In foopkg/foo.py module:
if typing.TYPE_CHECKING:
    from foopkg import _A, _B, _C

# In test_coverage.py:
import pytest

from vutils.testing.utils import cover_typing

# Ensure the test run as last (this feature is available after installing
# pytest-order). cover_typing reloads the module which may have negative
# consequences on other tests
@pytest.mark.order("last")
def test_typing_code_is_covered():
    # When called, following happens:
    # - typing.TYPE_CHECKING is patched to True
    # - foopkg is patched with _A, _B, and _C symbols if they do not exist
    # - finally, foopkg.foo is reloaded
    cover_typing("foopkg.foo", ["_A", "_B", "_C"])
```
The story behind `cover_typing` is to keep source files clean from directives
telling the `pytest` and linters what to do.

Sometimes a symbol can play two roles. Suppose that symbol `_L` is a type alias
for `list[object]` when `mypy` is performing its checks and `list` otherwise:
```python
# In foopkg/foo.py module:
if typing.TYPE_CHECKING:
    from foopkg import _L
else:
    _L = list


class ListType(_L):
    pass
```
To cover this case, `ClassLikeSymbol` from `vutils.testing.utils` comes to
help. In `test_coverage.py`, just define `_L` like
```python
class _L(metaclass=ClassLikeSymbol):
    pass
```
and then pass it to `cover_typing`:
```python
cover_typing("foopkg.foo", [_L])
```

### Deferred Instance Initialization

Patching may take no effect if the patched object appears in constructor and
this constructor is called outside of patcher context. `LazyInstance` from
`vutils.testing.utils` can defer initialization up to the time of method call:
```python
class StderrWriter:
    def __init__(self):
        self.stream = sys.stderr

    def write(self, text):
        self.stream.write(text)

class StderrPatcher(PatcherFactory):
    def setup(self):
        self.stream = io.StringIO
        self.add_spec("sys.stderr", new=self.stream)

class MyTestCase(TestCase):
    def test_deferred_initialization(self):
        writerA = StderrWriter()
        writerB = LazyInstance(StderrWriter).create()
        patcher = StderrPatcher()

        # Patch sys.stderr:
        with patcher.patch():
            # Write Hello! to standard error output:
            writerA.write("Hello!\n")
            # Write Hi! to StringIO instance:
            writerB.write("Hi!\n")
```

### Deferred `assertRaises`

Sometimes there are callable objects with a very similar prototypes and
behavior so they can be run and checked with one universal function. However,
if one of them raises an exception under specific circumstances, this must be
also handled by the universal function, which adds to its complexity. For this
reason, `vutils.testing.utils` introduces `AssertRaises` class which wraps
exception raising assertions:
```python
class FooError(Exception):
    detail = "foo"

def func_a(obj):
    obj.foo = 42

def func_b(obj):
    func_a(obj)
    raise FooError()

def Foo(TestCase):
    def run_and_check(self, func):
        obj = make_mock()
        func(obj)
        self.assertEqual(obj.foo, 42)

    def test_func(self):
        wfunc_b = AssertRaises(self, func_b, FooError)

        self.run_and_check(func_a)
        # Catch and store FooError:
        self.run_and_check(wfunc_b)
        # Check the caught exception:
        self.assertEqual(wfunc_b.get_exception().detail, "foo")
```

### Enhanced `TestCase`

Module `vutils.testing.testcase` provides `TestCase` which is a subclass of
`unittest.TestCase` extended about these methods:

* `assert_called_with` - assert that the mock object has been called once with
  the specified arguments and then reset it:
  ```python
  class ExampleTestCase(TestCase):
      def test_assert_called_with(self):
          mock = make_mock(["foo"])

          mock.foo(1, 2, bar=3)
          self.assert_called_with(mock, 1, 2, bar=3)

          mock.foo(4)
          self.assert_called_with(mock, 4)
  ```
* `assert_not_called` - assert that the mock object has not been called:
  ```python
  class ExampleTestCase(TestCase):
      def test_assert_not_called(self):
          mock = make_mock(["foo"])

          self.assert_not_called(mock.foo)
  ```

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/i386x/vutils-testing",
    "name": "vutils-testing",
    "maintainer": "",
    "docs_url": null,
    "requires_python": "<4,>=3.7",
    "maintainer_email": "",
    "keywords": "testing,mocking,unit testing",
    "author": "Ji\u0159\u00ed Ku\u010dera",
    "author_email": "sanczes@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/c4/ff/b0c37a701adba75ea6b71644a2955591a2bfc26c7d390d27efdff3c9cf34/vutils-testing-1.0.3.tar.gz",
    "platform": "any",
    "description": "[![Coverage Status](https://coveralls.io/repos/github/i386x/vutils-testing/badge.svg?branch=main)](https://coveralls.io/github/i386x/vutils-testing?branch=main)\n![CodeQL](https://github.com/i386x/vutils-testing/actions/workflows/codeql.yml/badge.svg)\n\n# vutils-testing: Auxiliary Library for Writing Tests\n\nThis package provides a set of tools that help with writing tests. It helps\nwith creating test data and types, mocking objects, patching, and verifying\ntest results.\n\n## Installation\n\nTo install the package, type\n```sh\n$ pip install vutils-testing\n```\n\n## How to Use\n\nFor more details, please follow the subsections below.\n\n### Type Factories\n\nSometimes tests require new types to be defined. To do this,\n`vutils.testing.utils` provides `make_type` function, which is a wrapper of\n`type`:\n```python\n# Create type derived directly from object:\nmy_type = make_type(\"MyType\")\n\n# Create class derived directly from Exception:\nmy_error = make_type(\"MyError\", Exception)\n\n# Create class derived from A and B:\nmy_class = make_type(\"MyClass\", (A, B))\n\n# Create class derived from A with foo member:\nmy_another_class = make_type(\"MyAnotherClass\", A, {\"foo\": 42})\n\n# Create class derived from object with foo member:\nmy_test_class = make_type(\"MyTestClass\", members={\"foo\": 42})\n\n# Key-value arguments other than bases and members are passed to\n# __init_subclass__:\nmy_fourth_class = make_type(\"MyFourthClass\", bases=A, foo=42)\n```\n\n### Mocking Objects and Patching\n\n`make_mock`, `make_callable`, and `PatcherFactory` from `vutils.testing.mock`\nallow to create mock objects and patching things.\n\n`make_mock(*args, **kwargs)` is a shortcut for `unittest.mock.Mock`\n\n`make_callable(x)` creates also instance of `unittest.mock.Mock`, but it\nspecifies its function-related behavior: if `x` is callable, it is used to do a\nside-effect, otherwise it is used as the return value.\n```python\n# func_a() returns 3\nfunc_a = make_callable(3)\n\ncontainer = []\n\n# func_b() appends 42 to container\nfunc_b = make_callable(lambda *x, **y: container.append(42))\n\n# func_c() returns func_b\nfunc_c = make_callable(lambda *x, **y: func_b)\n```\n\n`PatcherFactory` allows to use `unittest.mock.patch` multiple-times without\nneed of nested `with` statements. When instantiated, `setup` method is called.\n`setup` method, implemented in the subclass, then may define set of patcher\nspecifications via `add_spec` method:\n```python\nclass MyPatcher(PatcherFactory):\n\n    @staticmethod\n    def setup_foo(mock):\n        mock.foo = \"foo\"\n\n    @staticmethod\n    def setup_baz(baz):\n        baz[\"quux\"] = 42\n\n    def setup(self):\n        self.baz = {}\n        # When self.patch() is called:\n        # - create a mock object, apply setup_foo on it, and patch foopkg.foo\n        #   with it:\n        self.add_spec(\"foopkg.foo\", self.setup_foo)\n        # - patch foopkg.bar with 42:\n        self.add_spec(\"foopkg.bar\", new=42)\n        # - apply setup_baz on baz and patch foopkg.baz with it (create=True\n        #   and other possible key-value arguments are passed to\n        #   unittest.mock.patch):\n        self.add_spec(\"foopkg.baz\", self.setup_baz, new=self.baz, create=True)\n\npatcher = MyPatcher()\n\nwith patcher.patch():\n   # Patches are applied in order as specified by add_spec and reverted in\n   # reverse order.\n   do_something()\n```\n\n### Covering `mypy` Specific Code\n\nWhen a module contains code that is visible only to `mypy`, it is not executed\nduring unit tests and hence reported as not covered. Function `cover_typing`\nfrom `vutils.testing.utils` module has the ability to execute such a code and\ntherefore improve coverage reports:\n```python\n# In foopkg/foo.py module:\nif typing.TYPE_CHECKING:\n    from foopkg import _A, _B, _C\n\n# In test_coverage.py:\nimport pytest\n\nfrom vutils.testing.utils import cover_typing\n\n# Ensure the test run as last (this feature is available after installing\n# pytest-order). cover_typing reloads the module which may have negative\n# consequences on other tests\n@pytest.mark.order(\"last\")\ndef test_typing_code_is_covered():\n    # When called, following happens:\n    # - typing.TYPE_CHECKING is patched to True\n    # - foopkg is patched with _A, _B, and _C symbols if they do not exist\n    # - finally, foopkg.foo is reloaded\n    cover_typing(\"foopkg.foo\", [\"_A\", \"_B\", \"_C\"])\n```\nThe story behind `cover_typing` is to keep source files clean from directives\ntelling the `pytest` and linters what to do.\n\nSometimes a symbol can play two roles. Suppose that symbol `_L` is a type alias\nfor `list[object]` when `mypy` is performing its checks and `list` otherwise:\n```python\n# In foopkg/foo.py module:\nif typing.TYPE_CHECKING:\n    from foopkg import _L\nelse:\n    _L = list\n\n\nclass ListType(_L):\n    pass\n```\nTo cover this case, `ClassLikeSymbol` from `vutils.testing.utils` comes to\nhelp. In `test_coverage.py`, just define `_L` like\n```python\nclass _L(metaclass=ClassLikeSymbol):\n    pass\n```\nand then pass it to `cover_typing`:\n```python\ncover_typing(\"foopkg.foo\", [_L])\n```\n\n### Deferred Instance Initialization\n\nPatching may take no effect if the patched object appears in constructor and\nthis constructor is called outside of patcher context. `LazyInstance` from\n`vutils.testing.utils` can defer initialization up to the time of method call:\n```python\nclass StderrWriter:\n    def __init__(self):\n        self.stream = sys.stderr\n\n    def write(self, text):\n        self.stream.write(text)\n\nclass StderrPatcher(PatcherFactory):\n    def setup(self):\n        self.stream = io.StringIO\n        self.add_spec(\"sys.stderr\", new=self.stream)\n\nclass MyTestCase(TestCase):\n    def test_deferred_initialization(self):\n        writerA = StderrWriter()\n        writerB = LazyInstance(StderrWriter).create()\n        patcher = StderrPatcher()\n\n        # Patch sys.stderr:\n        with patcher.patch():\n            # Write Hello! to standard error output:\n            writerA.write(\"Hello!\\n\")\n            # Write Hi! to StringIO instance:\n            writerB.write(\"Hi!\\n\")\n```\n\n### Deferred `assertRaises`\n\nSometimes there are callable objects with a very similar prototypes and\nbehavior so they can be run and checked with one universal function. However,\nif one of them raises an exception under specific circumstances, this must be\nalso handled by the universal function, which adds to its complexity. For this\nreason, `vutils.testing.utils` introduces `AssertRaises` class which wraps\nexception raising assertions:\n```python\nclass FooError(Exception):\n    detail = \"foo\"\n\ndef func_a(obj):\n    obj.foo = 42\n\ndef func_b(obj):\n    func_a(obj)\n    raise FooError()\n\ndef Foo(TestCase):\n    def run_and_check(self, func):\n        obj = make_mock()\n        func(obj)\n        self.assertEqual(obj.foo, 42)\n\n    def test_func(self):\n        wfunc_b = AssertRaises(self, func_b, FooError)\n\n        self.run_and_check(func_a)\n        # Catch and store FooError:\n        self.run_and_check(wfunc_b)\n        # Check the caught exception:\n        self.assertEqual(wfunc_b.get_exception().detail, \"foo\")\n```\n\n### Enhanced `TestCase`\n\nModule `vutils.testing.testcase` provides `TestCase` which is a subclass of\n`unittest.TestCase` extended about these methods:\n\n* `assert_called_with` - assert that the mock object has been called once with\n  the specified arguments and then reset it:\n  ```python\n  class ExampleTestCase(TestCase):\n      def test_assert_called_with(self):\n          mock = make_mock([\"foo\"])\n\n          mock.foo(1, 2, bar=3)\n          self.assert_called_with(mock, 1, 2, bar=3)\n\n          mock.foo(4)\n          self.assert_called_with(mock, 4)\n  ```\n* `assert_not_called` - assert that the mock object has not been called:\n  ```python\n  class ExampleTestCase(TestCase):\n      def test_assert_not_called(self):\n          mock = make_mock([\"foo\"])\n\n          self.assert_not_called(mock.foo)\n  ```\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Auxiliary library for writing tests",
    "version": "1.0.3",
    "project_urls": {
        "Bug Reports": "https://github.com/i386x/vutils-testing/issues",
        "Homepage": "https://github.com/i386x/vutils-testing",
        "Source": "https://github.com/i386x/vutils-testing"
    },
    "split_keywords": [
        "testing",
        "mocking",
        "unit testing"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "e41fa8067c8c724b102cd97e9f30a4465ca5a4071e7573b5363b00185cfed5dc",
                "md5": "3b488392980c8465f8478da0d16abee3",
                "sha256": "b3b5be086ce208a36ab3c2bb918049e3dd654ad0f32a5dec476a04b4aa9aefcb"
            },
            "downloads": -1,
            "filename": "vutils_testing-1.0.3-py2.py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "3b488392980c8465f8478da0d16abee3",
            "packagetype": "bdist_wheel",
            "python_version": "py2.py3",
            "requires_python": "<4,>=3.7",
            "size": 13857,
            "upload_time": "2023-10-15T17:46:02",
            "upload_time_iso_8601": "2023-10-15T17:46:02.826489Z",
            "url": "https://files.pythonhosted.org/packages/e4/1f/a8067c8c724b102cd97e9f30a4465ca5a4071e7573b5363b00185cfed5dc/vutils_testing-1.0.3-py2.py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "c4ffb0c37a701adba75ea6b71644a2955591a2bfc26c7d390d27efdff3c9cf34",
                "md5": "5d6b13eece5431816f73c2b6d84b525e",
                "sha256": "b9315605aaf3558db8902cf9137d4e2a4c6df54d5a0e76538609b99958a8c49f"
            },
            "downloads": -1,
            "filename": "vutils-testing-1.0.3.tar.gz",
            "has_sig": false,
            "md5_digest": "5d6b13eece5431816f73c2b6d84b525e",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4,>=3.7",
            "size": 20862,
            "upload_time": "2023-10-15T17:46:04",
            "upload_time_iso_8601": "2023-10-15T17:46:04.454119Z",
            "url": "https://files.pythonhosted.org/packages/c4/ff/b0c37a701adba75ea6b71644a2955591a2bfc26c7d390d27efdff3c9cf34/vutils-testing-1.0.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-10-15 17:46:04",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "i386x",
    "github_project": "vutils-testing",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "tox": true,
    "lcname": "vutils-testing"
}
        
Elapsed time: 0.14437s