8000 feat(dev): Add object matcher pytest fixture (#890) · SingleTM/sentry-python@34f173f · GitHub
[go: up one dir, main page]

Skip to content

Commit 34f173f

Browse files
authored
feat(dev): Add object matcher pytest fixture (getsentry#890)
1 parent 5283055 commit 34f173f

File tree

2 files changed

+177
-3
lines changed

2 files changed

+177
-3
lines changed

tests/conftest.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ def string_containing_matcher():
341341
342342
Used like this:
343343
344-
>>> f = mock.Mo 10000 ck(return_value=None)
344+
>>> f = mock.Mock()
345345
>>> f("dogs are great")
346346
>>> f.assert_any_call("dogs") # will raise AssertionError
347347
Traceback (most recent call last):
@@ -359,6 +359,9 @@ def __eq__(self, test_string):
359359
if not isinstance(test_string, str):
360360
return False
361361

362+
if len(self.substring) > len(test_string):
363+
return False
364+
362365
return self.substring in test_string
363366

364367
return StringContaining
@@ -374,7 +377,7 @@ def dictionary_containing_matcher():
374377
375378
Used like this:
376379
377-
>>> f = mock.Mock(return_value=None)
380+
>>> f = mock.Mock()
378381
>>> f({"dogs": "yes", "cats": "maybe"})
379382
>>> f.assert_any_call({"dogs": "yes"}) # will raise AssertionError
380383
Traceback (most recent call last):
@@ -391,6 +394,67 @@ def __eq__(self, test_dict):
391394
if not isinstance(test_dict, dict):
392395
return False
393396

394-
return all(test_dict.get(key) == self.subdict[key] for key in self.subdict)
397+
if len(self.subdict) > len(test_dict):
398+
return False
399+
400+
# Have to test self == other (rather than vice-versa) in case
401+
# any of the values in self.subdict is another matcher with a custom
402+
# __eq__ method (in LHS == RHS, LHS's __eq__ is tried before RHS's).
403+
# In other words, this order is important so that examples like
404+
# {"dogs": "are great"} == DictionaryContaining({"dogs": StringContaining("great")})
405+
# evaluate to True
406+
return all(self.subdict[key] == test_dict.get(key) for key in self.subdict)
395407

396408
return DictionaryContaining
409+
410+
411+
@pytest.fixture(name="ObjectDescribedBy")
412+
def object_described_by_matcher():
413+
"""
414+
An object which matches any other object with the given properties.
415+
416+
Available properties currently are "type" (a type object) and "attrs" (a
417+
dictionary).
418+
419+
Useful for assert_called_with, assert_any_call, etc.
420+
421+
Used like this:
422+
423+
>>> class Dog(object):
424+
... pass
425+
...
426+
>>> maisey = Dog()
427+
>>> maisey.name = "Maisey"
428+
>>> maisey.age = 7
429+
>>> f = mock.Mock()
430+
>>> f(maisey)
431+
>>> f.assert_any_call(ObjectDescribedBy(type=Dog)) # no AssertionError
432+
>>> f.assert_any_call(ObjectDescribedBy(attrs={"name": "Maisey"})) # no AssertionError
433+
"""
434+
435+
class ObjectDescribedBy(object):
436+
def __init__(self, type=None, attrs=None):
437+
self.type = type
438+
self.attrs = attrs
439+
440+
def __eq__(self, test_obj):
441+
if self.type:
442+
if not isinstance(test_obj, self.type):
443+
return False
444+
445+
# all checks here done with getattr rather than comparing to
446+
# __dict__ because __dict__ isn't guaranteed to exist
447+
if self.attrs:
448+
# attributes must exist AND values must match
449+
try:
450+
if any(
451+
getattr(test_obj, attr_name) != attr_value
452+
for attr_name, attr_value in self.attrs.items()
453+
):
454+
return False # wrong attribute value
455+
except AttributeError: # missing attribute
456+
return False
457+
458+
return True
459+
460+
return ObjectDescribedBy

tests/test_conftest.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import pytest
2+
3+
4+
@pytest.mark.parametrize(
5+
"test_string, expected_result",
6+
[
7+
# type matches
8+
("dogs are great!", True), # full containment - beginning
9+
("go, dogs, go!", True), # full containment - middle
10+
("I like dogs", True), # full containment - end
11+
("dogs", True), # equality
12+
("", False), # reverse containment
13+
("dog", False), # reverse containment
14+
("good dog!", False), # partial overlap
15+
("cats", False), # no overlap
16+
# type mismatches
17+
(1231, False),
18+
(11.21, False),
19+
([], False),
20+
({}, False),
21+
(True, False),
22+
],
23+
)
24+
def test_string_containing(
25+
test_string, expected_result, StringContaining # noqa: N803
26+
):
27+
28+
assert (test_string == StringContaining("dogs")) is expected_result
29+
30+
31+
@pytest.mark.parametrize(
32+
"test_dict, expected_result",
33+
[
34+
# type matches
35+
({"dogs": "yes", "cats": "maybe", "spiders": "nope"}, True), # full containment
36+
({"dogs": "yes", "cats": "maybe"}, True), # equality
37+
({}, False), # reverse containment
38+
({"dogs": "yes"}, False), # reverse containment
39+
({"dogs": "yes", "birds": "only outside"}, False), # partial overlap
40+
({"coyotes": "from afar"}, False), # no overlap
41+
# type mismatches
42+
('{"dogs": "yes", "cats": "maybe"}', False),
43+
(1231, False),
44+
(11.21, False),
45+
([], False),
46+
(True, False),
47+
],
48+
)
49+
def test_dictionary_containing(
50+
test_dict, expected_result, DictionaryContaining # noqa: N803
51+
):
52+
53+
assert (
54+
test_dict == DictionaryContaining({"dogs": "yes", "cats": "maybe"})
55+
) is expected_result
56+
57+
58+
class Animal(object): # noqa: B903
59+
def __init__(self, name=None, age=None, description=None):
60+
self.name = name
61+
self.age = age
62+
self.description = description
63+
64+
65+
class Dog(Animal):
66+
pass
67+
68+
69+
class Cat(Animal):
70+
pass
71+
72+
73+
@pytest.mark.parametrize(
74+
"test_obj, type_and_attrs_result, type_only_result, attrs_only_result",
75+
[
76+
# type matches
77+
(Dog("Maisey", 7, "silly"), True, True, True), # full attr containment
78+
(Dog("Maisey", 7), True, True, True), # type and attr equality
79+
(Dog(), False, True, False), # reverse attr containment
80+
(Dog("Maisey"), False, True, False), # reverse attr containment
81+
(Dog("Charlie", 7, "goofy"), False, True, False), # partial attr overlap
82+
(Dog("Bodhi", 6, "floppy"), False, True, False), # no attr overlap
83+
# type mismatches
84+
(Cat("Maisey", 7), False, False, True), # attr equality
85+
(Cat("Piper", 1, "doglike"), False, False, False),
86+
("Good girl, Maisey", False, False, False),
87+
({"name": "Maisey", "age": 7}, False, False, False),
88+
(1231, False, False, False),
89+
(11.21, False, False, False),
90+
([], False, False, False),
91+
(True, False, False, False),
92+
],
93+
)
94+
def test_object_described_by(
95+
test_obj,
96+
type_and_attrs_result,
97+
type_only_result,
98+
attrs_only_result,
99+
ObjectDescribedBy, # noqa: N803
100+
):
101+
102+
assert (
103+
test_obj == ObjectDescribedBy(type=Dog, attrs={"name": "Maisey", "age": 7})
104+
) is type_and_attrs_result
105+
106+
assert (test_obj == ObjectDescribedBy(type=Dog)) is type_only_result
107+
108+
assert (
109+
test_obj == ObjectDescribedBy(attrs={"name": "Maisey", "age": 7})
110+
) is attrs_only_result

0 commit comments

Comments
 (0)
0