From 4c4c11977c70d645d13ff36f45ff886589fd7ada Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Thu, 22 Oct 2020 07:48:07 -0700 Subject: [PATCH 1/2] add object-matcher fixture, tweak others --- tests/conftest.py | 70 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2d77b41d19..6c53e502ef 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -341,7 +341,7 @@ def string_containing_matcher(): Used like this: - >>> f = mock.Mock(return_value=None) + >>> f = mock.Mock() >>> f("dogs are great") >>> f.assert_any_call("dogs") # will raise AssertionError Traceback (most recent call last): @@ -359,6 +359,9 @@ def __eq__(self, test_string): if not isinstance(test_string, str): return False + if len(self.substring) > len(test_string): + return False + return self.substring in test_string return StringContaining @@ -374,7 +377,7 @@ def dictionary_containing_matcher(): Used like this: - >>> f = mock.Mock(return_value=None) + >>> f = mock.Mock() >>> f({"dogs": "yes", "cats": "maybe"}) >>> f.assert_any_call({"dogs": "yes"}) # will raise AssertionError Traceback (most recent call last): @@ -391,6 +394,67 @@ def __eq__(self, test_dict): if not isinstance(test_dict, dict): return False - return all(test_dict.get(key) == self.subdict[key] for key in self.subdict) + if len(self.subdict) > len(test_dict): + return False + + # Have to test self == other (rather than vice-versa) in case + # any of the values in self.subdict is another matcher with a custom + # __eq__ method (in LHS == RHS, LHS's __eq__ is tried before RHS's). + # In other words, this order is important so that examples like + # {"dogs": "are great"} == DictionaryContaining({"dogs": StringContaining("great")}) + # evaluate to True + return all(self.subdict[key] == test_dict.get(key) for key in self.subdict) return DictionaryContaining + + +@pytest.fixture(name="ObjectDescribedBy") +def object_described_by_matcher(): + """ + An object which matches any other object with the given properties. + + Available properties currently are "type" (a type object) and "attrs" (a + dictionary). + + Useful for assert_called_with, assert_any_call, etc. + + Used like this: + + >>> class Dog(object): + ... pass + ... + >>> maisey = Dog() + >>> maisey.name = "Maisey" + >>> maisey.age = 7 + >>> f = mock.Mock() + >>> f(maisey) + >>> f.assert_any_call(ObjectDescribedBy(type=Dog)) # no AssertionError + >>> f.assert_any_call(ObjectDescribedBy(attrs={"name": "Maisey"})) # no AssertionError + """ + + class ObjectDescribedBy(object): + def __init__(self, type=None, attrs=None): + self.type = type + self.attrs = attrs + + def __eq__(self, test_obj): + if self.type: + if not isinstance(test_obj, self.type): + return False + + # all checks here done with getattr rather than comparing to + # __dict__ because __dict__ isn't guaranteed to exist + if self.attrs: + # attributes must exist AND values must match + try: + if any( + getattr(test_obj, attr_name) != attr_value + for attr_name, attr_value in self.attrs.items() + ): + return False # wrong attribute value + except AttributeError: # missing attribute + return False + + return True + + return ObjectDescribedBy From 4b15da219e432b15029cf7c90813a0441ff518f7 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 26 Oct 2020 11:50:29 -0700 Subject: [PATCH 2/2] add tests --- tests/test_conftest.py | 110 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 tests/test_conftest.py diff --git a/tests/test_conftest.py b/tests/test_conftest.py new file mode 100644 index 0000000000..8a2d4cee24 --- /dev/null +++ b/tests/test_conftest.py @@ -0,0 +1,110 @@ +import pytest + + +@pytest.mark.parametrize( + "test_string, expected_result", + [ + # type matches + ("dogs are great!", True), # full containment - beginning + ("go, dogs, go!", True), # full containment - middle + ("I like dogs", True), # full containment - end + ("dogs", True), # equality + ("", False), # reverse containment + ("dog", False), # reverse containment + ("good dog!", False), # partial overlap + ("cats", False), # no overlap + # type mismatches + (1231, False), + (11.21, False), + ([], False), + ({}, False), + (True, False), + ], +) +def test_string_containing( + test_string, expected_result, StringContaining # noqa: N803 +): + + assert (test_string == StringContaining("dogs")) is expected_result + + +@pytest.mark.parametrize( + "test_dict, expected_result", + [ + # type matches + ({"dogs": "yes", "cats": "maybe", "spiders": "nope"}, True), # full containment + ({"dogs": "yes", "cats": "maybe"}, True), # equality + ({}, False), # reverse containment + ({"dogs": "yes"}, False), # reverse containment + ({"dogs": "yes", "birds": "only outside"}, False), # partial overlap + ({"coyotes": "from afar"}, False), # no overlap + # type mismatches + ('{"dogs": "yes", "cats": "maybe"}', False), + (1231, False), + (11.21, False), + ([], False), + (True, False), + ], +) +def test_dictionary_containing( + test_dict, expected_result, DictionaryContaining # noqa: N803 +): + + assert ( + test_dict == DictionaryContaining({"dogs": "yes", "cats": "maybe"}) + ) is expected_result + + +class Animal(object): # noqa: B903 + def __init__(self, name=None, age=None, description=None): + self.name = name + self.age = age + self.description = description + + +class Dog(Animal): + pass + + +class Cat(Animal): + pass + + +@pytest.mark.parametrize( + "test_obj, type_and_attrs_result, type_only_result, attrs_only_result", + [ + # type matches + (Dog("Maisey", 7, "silly"), True, True, True), # full attr containment + (Dog("Maisey", 7), True, True, True), # type and attr equality + (Dog(), False, True, False), # reverse attr containment + (Dog("Maisey"), False, True, False), # reverse attr containment + (Dog("Charlie", 7, "goofy"), False, True, False), # partial attr overlap + (Dog("Bodhi", 6, "floppy"), False, True, False), # no attr overlap + # type mismatches + (Cat("Maisey", 7), False, False, True), # attr equality + (Cat("Piper", 1, "doglike"), False, False, False), + ("Good girl, Maisey", False, False, False), + ({"name": "Maisey", "age": 7}, False, False, False), + (1231, False, False, False), + (11.21, False, False, False), + ([], False, False, False), + (True, False, False, False), + ], +) +def test_object_described_by( + test_obj, + type_and_attrs_result, + type_only_result, + attrs_only_result, + ObjectDescribedBy, # noqa: N803 +): + + assert ( + test_obj == ObjectDescribedBy(type=Dog, attrs={"name": "Maisey", "age": 7}) + ) is type_and_attrs_result + + assert (test_obj == ObjectDescribedBy(type=Dog)) is type_only_result + + assert ( + test_obj == ObjectDescribedBy(attrs={"name": "Maisey", "age": 7}) + ) is attrs_only_result