Skip to content

Commit 3034820

Browse files
authored
add support for NonCallableMock and AsyncMock (#2)
* add support for NonCallableMock and AsyncMock * add check that the class names being checked for are mock classes * switch to using class names
1 parent ef88d62 commit 3034820

File tree

5 files changed

+155
-6
lines changed

5 files changed

+155
-6
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@
22

33
## [Unreleased]
44

5+
## [v1.1.0] - 2023-01-14
6+
7+
### Added
8+
9+
- Lint checks that ensure `NonCallableMock` and `AsyncMock` constructors have
10+
the `spec` or `spec_set` argument
11+
512
## [v1.0.0] - 2023-01-14
613

714
### Added
815

916
- Lint checks that ensure `Mock` and `MagicMock` constructors have the `spec`
10-
argument
17+
or `spec_set`argument
1118

1219
[//]: # "Release links"
1320
[v1.0.0]: https://github.com/jdkandersson/flake8-mock-spec/releases/v1.0.0
21+
[v1.1.0]: https://github.com/jdkandersson/flake8-mock-spec/releases/v1.1.0

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ A few rules have been defined to allow for selective suppression:
5353
`spec` or `spec_set` argument.
5454
* `TMS002`: checks that `unittest.mock.MagicMock` instances are constructed with
5555
the `spec` or `spec_set` argument.
56+
* `TMS003`: checks that `unittest.mock.NonCallableMock` instances are
57+
constructed with the `spec` or `spec_set` argument.
58+
* `TMS004`: checks that `unittest.mock.AsyncMock` instances are constructed
59+
with the `spec` or `spec_set` argument.
5660

5761
### Fix TMS001
5862

@@ -119,3 +123,69 @@ from foo import Foo
119123
def test_foo():
120124
mocked_foo = mock.MagicMock(spec_set=Foo)
121125
```
126+
127+
### Fix TMS003
128+
129+
This linting rule is triggered by creating a `unittest.mock.NonCallableMock`
130+
instance without the `spec` or `spec_set` argument. For example:
131+
132+
```Python
133+
from unittest import mock
134+
135+
def test_foo():
136+
mocked_foo = mock.NonCallableMock()
137+
```
138+
139+
This example can be fixed by using the `spec` or `spec_set` argument in the
140+
constructor:
141+
142+
```Python
143+
from unittest import mock
144+
145+
from foo import Foo
146+
147+
def test_foo():
148+
mocked_foo = mock.NonCallableMock(spec=Foo)
149+
```
150+
151+
```Python
152+
from unittest import mock
153+
154+
from foo import Foo
155+
156+
def test_foo():
157+
mocked_foo = mock.NonCallableMock(spec_set=Foo)
158+
```
159+
160+
### Fix TMS004
161+
162+
This linting rule is triggered by creating a `unittest.mock.AsyncMock` instance
163+
without the `spec` or `spec_set` argument. For example:
164+
165+
```Python
166+
from unittest import mock
167+
168+
def test_foo():
169+
mocked_foo = mock.AsyncMock()
170+
```
171+
172+
This example can be fixed by using the `spec` or `spec_set` argument in the
173+
constructor:
174+
175+
```Python
176+
from unittest import mock
177+
178+
from foo import Foo
179+
180+
def test_foo():
181+
mocked_foo = mock.AsyncMock(spec=Foo)
182+
```
183+
184+
```Python
185+
from unittest import mock
186+
187+
from foo import Foo
188+
189+
def test_foo():
190+
mocked_foo = mock.AsyncMock(spec_set=Foo)
191+
```

flake8_mock_spec.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44

55
import ast
66
from typing import Iterator, NamedTuple
7+
from unittest import mock
78

8-
MOCK_CLASS = "Mock"
9-
MAGIC_MOCK_CLASS = "MagicMock"
10-
MOCK_CLASSES = frozenset((MOCK_CLASS, MAGIC_MOCK_CLASS))
9+
MOCK_CLASS: str = mock.Mock.__name__
10+
MAGIC_MOCK_CLASS: str = mock.MagicMock.__name__
11+
NON_CALLABLE_MOCK_CLASS: str = mock.NonCallableMock.__name__
12+
ASYNC_MOCK_CLASS: str = mock.AsyncMock.__name__
13+
MOCK_CLASSES = frozenset((MOCK_CLASS, MAGIC_MOCK_CLASS, NON_CALLABLE_MOCK_CLASS, ASYNC_MOCK_CLASS))
1114
SPEC_ARGS = frozenset(("spec", "spec_set"))
1215

1316
ERROR_CODE_PREFIX = "TMS"
@@ -24,6 +27,18 @@
2427
MAGIC_MOCK_CLASS,
2528
MAGIC_MOCK_SPEC_CODE.lower(),
2629
)
30+
NON_CALLABLE_MOCK_SPEC_CODE = f"{ERROR_CODE_PREFIX}003"
31+
NON_CALLABLE_MOCK_SPEC_MSG = MOCK_SPEC_MSG_BASE % (
32+
NON_CALLABLE_MOCK_SPEC_CODE,
33+
NON_CALLABLE_MOCK_CLASS,
34+
NON_CALLABLE_MOCK_SPEC_CODE.lower(),
35+
)
36+
ASYNC_MOCK_SPEC_CODE = f"{ERROR_CODE_PREFIX}004"
37+
ASYNC_MOCK_SPEC_MSG = MOCK_SPEC_MSG_BASE % (
38+
ASYNC_MOCK_SPEC_CODE,
39+
ASYNC_MOCK_CLASS,
40+
ASYNC_MOCK_SPEC_CODE.lower(),
41+
)
2742

2843

2944
class Problem(NamedTuple):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "flake8-mock-spec"
3-
version = "1.0.0"
3+
version = "1.1.0"
44
description = "A linter that checks mocks are constructed with the spec argument"
55
authors = ["David Andersson <[email protected]>"]
66
license = "Apache 2.0"

tests/test_flake8_mock_spec_unit.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
from __future__ import annotations
44

55
import ast
6+
from unittest import mock
67

78
import pytest
89

9-
from flake8_mock_spec import MAGIC_MOCK_SPEC_MSG, MOCK_SPEC_MSG, Plugin
10+
from flake8_mock_spec import MAGIC_MOCK_SPEC_MSG, MOCK_CLASSES, MOCK_SPEC_MSG, Plugin
1011

1112

1213
def _result(code: str) -> tuple[str, ...]:
@@ -43,6 +44,20 @@ def _result(code: str) -> tuple[str, ...]:
4344
),
4445
pytest.param(
4546
"""
47+
NonCallableMock()
48+
""",
49+
(f"2:0 {MAGIC_MOCK_SPEC_MSG}",),
50+
id="module NonCallableMock no spec",
51+
),
52+
pytest.param(
53+
"""
54+
AsyncMock()
55+
""",
56+
(f"2:0 {MAGIC_MOCK_SPEC_MSG}",),
57+
id="module AsyncMock no spec",
58+
),
59+
pytest.param(
60+
"""
4661
mock.Mock()
4762
""",
4863
(f"2:0 {MOCK_SPEC_MSG}",),
@@ -128,6 +143,34 @@ def test_():
128143
),
129144
pytest.param(
130145
"""
146+
NonCallableMock(spec=1)
147+
""",
148+
(),
149+
id="module NonCallableMock spec",
150+
),
151+
pytest.param(
152+
"""
153+
NonCallableMock(spec_set=1)
154+
""",
155+
(),
156+
id="module NonCallableMock spec_set",
157+
),
158+
pytest.param(
159+
"""
160+
AsyncMock(spec=1)
161+
""",
162+
(),
163+
id="module AsyncMock spec",
164+
),
165+
pytest.param(
166+
"""
167+
AsyncMock(spec_set=1)
168+
""",
169+
(),
170+
id="module AsyncMock spec_set",
171+
),
172+
pytest.param(
173+
"""
131174
Other()
132175
""",
133176
(),
@@ -149,3 +192,16 @@ def test_plugin(code: str, expected_result: tuple[str, ...]):
149192
then: the expected result is returned
150193
"""
151194
assert _result(code) == expected_result
195+
196+
197+
@pytest.mark.parametrize(
198+
"class_", [pytest.param(class_, id=f"{class_} class") for class_ in MOCK_CLASSES]
199+
)
200+
def test_mock_classes_exist(class_: str):
201+
"""
202+
given: mock class
203+
when: the existence of the class on unittest.mock is checked
204+
then: the class exists in mock and can be instantiated
205+
"""
206+
assert hasattr(mock, class_)
207+
getattr(mock, class_)()

0 commit comments

Comments
 (0)