Skip to content

Commit

Permalink
Merge 30f29c8 into c47bf38
Browse files Browse the repository at this point in the history
  • Loading branch information
deathowl committed Jan 7, 2021
2 parents c47bf38 + 30f29c8 commit 3bce7c6
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 2 deletions.
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ else
UNITTEST_ARGS := --verbose
endif
TESTS_SRCS = tests
SRCS = testslide util
SRCS = testslide util pytest-testslide
ALL_SRCS = $(TESTS_SRCS) $(SRCS)
TERM_BRIGHT := $(shell tput bold)
TERM_NONE := $(shell tput sgr0)
Expand Down Expand Up @@ -68,6 +68,16 @@ docs_clean:
.PHONY: unittest_tests
unittest_tests: $(TESTS_SRCS)/*_unittest.py

.PHONY: pytest_tests
pytest_tests: export PYTHONPATH=${CURDIR}/pytest-testslide:${CURDIR}
pytest_tests: FORCE coverage_erase
@printf "${TERM_BRIGHT}INSTALL pytest_testslide DEPS ${TERM_NONE}\n"
${Q} pip install -r pytest-testslide/requirements.txt
@printf "${TERM_BRIGHT}PYTEST pytest_testslide${TERM_NONE}"
${Q} coverage run \
-m pytest \
pytest-testslide/tests

%_testslide.py: FORCE coverage_erase
@printf "${TERM_BRIGHT}TESTSLIDE $@\n${TERM_NONE}"
${Q} coverage run \
Expand Down Expand Up @@ -120,6 +130,7 @@ format_black:
tests: \
unittest_tests \
testslide_tests \
pytest_tests \
mypy \
flake8 \
isort \
Expand Down
6 changes: 5 additions & 1 deletion mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ ignore_missing_imports = True
ignore_missing_imports = True
[mypy-pygments.*]
ignore_missing_imports = True
[mypy-pytest.*]
ignore_missing_imports = True
[mypy-setuptools.*]
ignore_missing_imports = True
[mypy-testslide.testslide]
disallow_untyped_calls = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
disallow_untyped_decorators = True
2 changes: 2 additions & 0 deletions pytest-testslide/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include testslide-version
include requirements.txt
39 changes: 39 additions & 0 deletions pytest-testslide/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[TestSlide](https://testslide.readthedocs.io/) fixture for pytest.

## Quickstart

Install:

```
pip install pytest-testslide
```

In your test file:
```
import pytest
from pytest_testslide import testslide
from testslide import StrictMock # if you wish to use StrictMock
from testslide import matchers # if you wish to use Rspec style argument matchers
.....
def test_mock_callable_patching_works(testslide):
testslide.mock_callable(time, "sleep").to_raise(RuntimeError("Mocked!")) #mock_callable
with pytest.raises(RuntimeError):
time.sleep()
@pytest.mark.asyncio
async def test_mock_async_callable_patching_works(testslide):
testslide.mock_async_callable(sample_module.ParentTarget, "async_static_method").to_raise(RuntimeError("Mocked!")) #mock_async_callable
with pytest.raises(RuntimeError):
await sample_module.ParentTarget.async_static_method("a", "b")
def test_mock_constructor_patching_works(testslide):
testslide.mock_constructor(sample_module, "ParentTarget").to_raise(RuntimeError("Mocked!")) #mock_constructor
with pytest.raises(RuntimeError):
sample_module.ParentTarget()
def test_patch_attribute_patching_works(testslide):
testslide.patch_attribute(sample_module.SomeClass, "attribute", "patched") #patch_attribute
assert sample_module.SomeClass.attribute == "patched"
```

70 changes: 70 additions & 0 deletions pytest-testslide/pytest_testslide.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

from types import TracebackType
from typing import Any, Callable, Iterator, List, Optional

import pytest

import testslide as testslide_module


class _TestSlideFixture:
def _register_assertion(self, assertion: Callable) -> None:
self._assertions.append(assertion)

def __enter__(self) -> "_TestSlideFixture":
self._assertions: List[Callable] = []
testslide_module.mock_callable.register_assertion = self._register_assertion
return self

def __exit__(
self,
exc_type: Optional[type],
exc_val: Optional[Exception],
exc_tb: TracebackType,
):
aggregated_exceptions = testslide_module.AggregatedExceptions()
try:
for assertion in self._assertions:
try:
assertion()
except BaseException as be:
aggregated_exceptions.append_exception(be)

finally:
testslide_module.mock_callable.unpatch_all_callable_mocks()
testslide_module.mock_constructor.unpatch_all_constructor_mocks()
testslide_module.patch_attribute.unpatch_all_mocked_attributes()
if aggregated_exceptions.exceptions:
pytest.fail(str(aggregated_exceptions), False)

@staticmethod
def mock_callable(
*args: Any, **kwargs: Any
) -> testslide_module.mock_callable._MockCallableDSL:
return testslide_module.mock_callable.mock_callable(*args, **kwargs)

@staticmethod
def mock_async_callable(
*args: Any, **kwargs: Any
) -> testslide_module.mock_callable._MockAsyncCallableDSL:
return testslide_module.mock_callable.mock_async_callable(*args, **kwargs)

@staticmethod
def mock_constructor(
*args: Any, **kwargs: Any
) -> testslide_module.mock_constructor._MockConstructorDSL:
return testslide_module.mock_constructor.mock_constructor(*args, **kwargs)

@staticmethod
def patch_attribute(*args: Any, **kwargs: Any) -> None:
return testslide_module.patch_attribute.patch_attribute(*args, **kwargs)


@pytest.fixture
def testslide() -> Iterator[_TestSlideFixture]:
with _TestSlideFixture() as testslide_fixture:
yield testslide_fixture
2 changes: 2 additions & 0 deletions pytest-testslide/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pytest~=6.2
pytest-asyncio>=0.14.0
49 changes: 49 additions & 0 deletions pytest-testslide/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

import os

from setuptools import setup

dir_path = os.path.dirname(os.path.realpath(__file__))

ts_version = (
open(os.path.join(dir_path, os.pardir, "testslide_version")).read().rstrip()
)
requirements = open("requirements.txt", encoding="utf8").readlines()

with open("README.md", encoding="utf8") as f:
readme = f.read()

setup(
name="pytest-testslide",
version=ts_version,
py_modules=["pytest_testslide"],
maintainer="Balint Csergo",
maintainer_email="deathowlzz@gmail.com",
url="https://github.com/facebookincubator/TestSlide/tree/master/pytest-testslide",
license="MIT",
description="TestSlide fixture for pytest",
long_description=readme,
long_description_content_type="text/markdown",
setup_requires=["setuptools>=38.6.0"],
install_requires=[
f"testslide>={ts_version}",
]
+ requirements,
extras_require={"build": ["black", "flake8", "mypy"]},
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Topic :: Software Development :: Testing",
"Topic :: Software Development :: Testing :: Acceptance",
"Topic :: Software Development :: Testing :: BDD",
"Topic :: Software Development :: Testing :: Mocking",
"Topic :: Software Development :: Testing :: Unit ",
],
)
6 changes: 6 additions & 0 deletions pytest-testslide/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

pytest_plugins = "pytester"
168 changes: 168 additions & 0 deletions pytest-testslide/tests/test_pytest_testslide.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
import re


def test_pass(testdir):
testdir.makepyfile(
"""
import time
import pytest
from pytest_testslide import testslide
from tests import sample_module
from testslide import StrictMock
def test_has_mock_callable(testslide):
testslide.mock_callable
def test_mock_callable_assertion_works(testslide):
testslide.mock_callable
def test_mock_callable_unpaches(testslide):
testslide.mock_callable
def test_has_mock_async_callable(testslide):
testslide.mock_async_callable
def test_mock_async_callable_assertion_works(testslide):
testslide.mock_async_callable
def test_mock_async_callable_unpaches(testslide):
testslide.mock_async_callable
def test_has_mock_constructor(testslide):
testslide.mock_constructor
def test_mock_constructor_assertion_works(testslide):
testslide.mock_constructor
def test_mock_constructor_unpaches(testslide):
testslide.mock_constructor
def test_has_patch_attribute(testslide):
testslide.patch_attribute
def test_patch_attribute_unpaches(testslide):
testslide.patch_attribute
# mock_callable integration tests
def test_mock_callable_patching_works(testslide):
testslide.mock_callable(time, "sleep").to_raise(RuntimeError("Mocked!"))
with pytest.raises(RuntimeError):
time.sleep()
def test_mock_callable_unpatching_works(testslide):
# This will fail if unpatching from test_mock_callable_patching_works does
# not happen
time.sleep(0)
def test_mock_callable_assertion_works(testslide):
testslide.mock_callable("time", "sleep").for_call(0).to_call_original().and_assert_called_once()
time.sleep(0)
def test_mock_callable_failing_assertion_works(testslide):
testslide.mock_callable("time", "sleep").for_call(0).to_call_original().and_assert_called_once()
# mock_async_callable integration test
@pytest.mark.asyncio
async def test_mock_async_callable_patching_works(testslide):
testslide.mock_async_callable(sample_module.ParentTarget, "async_static_method").to_raise(RuntimeError("Mocked!"))
with pytest.raises(RuntimeError):
await sample_module.ParentTarget.async_static_method("a", "b")
@pytest.mark.asyncio
async def test_mock_async_callable_unpatching_works(testslide):
# This will fail if unpatching from test_mock_async_callable_patching_works does
# not happen
assert await sample_module.ParentTarget.async_static_method("a", "b") == "async original response"
@pytest.mark.asyncio
async def test_mock_async_callable_assertion_works(testslide):
testslide.mock_async_callable(sample_module.ParentTarget, "async_static_method").for_call("a", "b").to_call_original().and_assert_called_once()
await sample_module.ParentTarget.async_static_method("a", "b")
def test_mock_async_callable_failing_assertion_works(testslide):
testslide.mock_async_callable(sample_module.ParentTarget, "async_static_method").for_call("a", "b").to_call_original().and_assert_called_once()
# mock_constructor integration test
def test_mock_constructor_patching_works(testslide):
testslide.mock_constructor(sample_module, "ParentTarget").to_raise(RuntimeError("Mocked!"))
with pytest.raises(RuntimeError):
sample_module.ParentTarget()
def test_mock_constructor_unpatching_works(testslide):
# This will fail if unpatching from test_mock_constructor_patching_works does
# not happen
assert sample_module.ParentTarget()
def test_mock_constructor_assertion_works(testslide):
testslide.mock_constructor(sample_module, "ParentTarget").to_call_original().and_assert_called_once()
sample_module.ParentTarget()
def test_mock_constructor_failing_assertion_works(testslide):
testslide.mock_constructor(sample_module, "ParentTarget").to_call_original().and_assert_called_once()
# patch_attribute integration test
def test_patch_attribute_patching_works(testslide):
testslide.patch_attribute(sample_module.SomeClass, "attribute", "patched")
assert sample_module.SomeClass.attribute == "patched"
def test_patch_attribute_unpatching_works(testslide):
# This will fail if unpatching from test_mock_callable_patching_works does
# not happen
assert sample_module.SomeClass.attribute == "value"
def test_aggregated_exceptions(testslide):
mocked_cls = StrictMock(sample_module.CallOrderTarget)
testslide.mock_callable(mocked_cls, 'f1')\
.for_call("a").to_return_value("mocked")\
.and_assert_called_once()
testslide.mock_callable(mocked_cls, 'f1')\
.for_call("b").to_return_value("mocked2")\
.and_assert_called_once()
sample_module.CallOrderTarget("c").f1("a")
"""
)
result = testdir.runpytest("-v")
assert "passed, 4 errors" in result.stdout.str()
assert "failed" not in result.stdout.str()
expected_failure = re.compile(
""".*_______ ERROR at teardown of test_mock_callable_failing_assertion_works ________
1 failures.
<class \'AssertionError\'>: calls did not match assertion.
\'time\', \'sleep\':
expected: called exactly 1 time\(s\) with arguments:
\(0,\)
received: 0 call\(s\)
____ ERROR at teardown of test_mock_async_callable_failing_assertion_works _____
1 failures.
<class \'AssertionError\'>: calls did not match assertion.
<class \'tests.sample_module.ParentTarget\'>, \'async_static_method\':
expected: called exactly 1 time\(s\) with arguments:
\(\'a\', \'b\'\)
received: 0 call\(s\)
______ ERROR at teardown of test_mock_constructor_failing_assertion_works ______
1 failures.
<class \'AssertionError\'>: calls did not match assertion.
<class \'testslide.mock_constructor.ParentTarget\'>, \'__new__\':
expected: called exactly 1 time\(s\) with any arguments received: 0 call\(s\)
_______________ ERROR at teardown of test_aggregated_exceptions ________________
2 failures.
<class \'AssertionError\'>: calls did not match assertion.
<StrictMock 0x[a-fA-F0-9]+ template=tests.sample_module.CallOrderTarget .*/test_pass0/test_pass.py:[0-9]+>, \'f1\':
expected: called exactly 1 time\(s\) with arguments:
\(\'a\',\)
received: 0 call\(s\)
<class \'AssertionError\'>: calls did not match assertion.
<StrictMock 0x[a-fA-F0-9]+ template=tests.sample_module.CallOrderTarget .*/test_pass0/test_pass.py:[0-9]+>, \'f1\':
expected: called exactly 1 time\(s\) with arguments:
\(\'b\',\)
received: 0 call\(s\).*""",
re.MULTILINE | re.DOTALL,
)
assert expected_failure.match(result.stdout.str())
assert result.ret != 0
1 change: 1 addition & 0 deletions pytest-testslide/testslide-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.6.4

0 comments on commit 3bce7c6

Please sign in to comment.