Add a hacking check for test method closures
A recurring pattern when using multiple mocks is to create a closure decorated with mocks like: def test_thing(self): @mock.patch.object(self.compute, 'foo') @mock.patch.object(self.compute, 'bar') def _do_test(mock_bar, mock_foo): # Test things _do_test() However it is easy to leave off the _do_test() and have the test pass because nothing runs. This check looks for that pattern. Co-Authored-By: Andrew Laski <andrew@lascii.com> Change-Id: I4c2395a01470acc7c9e5bcf1d3578d00270a2c07
This commit is contained in:
parent
3ad46e3c78
commit
f6e4713bf6
@ -59,6 +59,7 @@ Nova Specific Commandments
|
|||||||
- [N346] Python 3: do not use dict.itervalues.
|
- [N346] Python 3: do not use dict.itervalues.
|
||||||
- [N347] Provide enough help text for config options
|
- [N347] Provide enough help text for config options
|
||||||
- [N348] Deprecated library function os.popen()
|
- [N348] Deprecated library function os.popen()
|
||||||
|
- [N349] Check for closures in tests which are not used
|
||||||
|
|
||||||
Creating Unit Tests
|
Creating Unit Tests
|
||||||
-------------------
|
-------------------
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import pep8
|
import pep8
|
||||||
@ -469,6 +470,77 @@ class CheckForTransAdd(BaseASTChecker):
|
|||||||
super(CheckForTransAdd, self).generic_visit(node)
|
super(CheckForTransAdd, self).generic_visit(node)
|
||||||
|
|
||||||
|
|
||||||
|
class _FindVariableReferences(ast.NodeVisitor):
|
||||||
|
def __init__(self):
|
||||||
|
super(_FindVariableReferences, self).__init__()
|
||||||
|
self._references = []
|
||||||
|
|
||||||
|
def visit_Name(self, node):
|
||||||
|
if isinstance(node.ctx, ast.Load):
|
||||||
|
# This means the value of a variable was loaded. For example a
|
||||||
|
# variable 'foo' was used like:
|
||||||
|
# mocked_thing.bar = foo
|
||||||
|
# foo()
|
||||||
|
# self.assertRaises(excepion, foo)
|
||||||
|
self._references.append(node.id)
|
||||||
|
super(_FindVariableReferences, self).generic_visit(node)
|
||||||
|
|
||||||
|
|
||||||
|
class CheckForUncalledTestClosure(BaseASTChecker):
|
||||||
|
"""Look for closures that are never called in tests.
|
||||||
|
|
||||||
|
A recurring pattern when using multiple mocks is to create a closure
|
||||||
|
decorated with mocks like:
|
||||||
|
|
||||||
|
def test_thing(self):
|
||||||
|
@mock.patch.object(self.compute, 'foo')
|
||||||
|
@mock.patch.object(self.compute, 'bar')
|
||||||
|
def _do_test(mock_bar, mock_foo):
|
||||||
|
# Test things
|
||||||
|
_do_test()
|
||||||
|
|
||||||
|
However it is easy to leave off the _do_test() and have the test pass
|
||||||
|
because nothing runs. This check looks for methods defined within a test
|
||||||
|
method and ensures that there is a reference to them. Only methods defined
|
||||||
|
one level deep are checked. Something like:
|
||||||
|
|
||||||
|
def test_thing(self):
|
||||||
|
class FakeThing:
|
||||||
|
def foo(self):
|
||||||
|
|
||||||
|
would not ensure that foo is referenced.
|
||||||
|
|
||||||
|
N349
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, tree, filename):
|
||||||
|
super(CheckForUncalledTestClosure, self).__init__(tree, filename)
|
||||||
|
self._filename = filename
|
||||||
|
|
||||||
|
def visit_FunctionDef(self, node):
|
||||||
|
# self._filename is 'stdin' in the unit test for this check.
|
||||||
|
if (not os.path.basename(self._filename).startswith('test_') and
|
||||||
|
not 'stdin'):
|
||||||
|
return
|
||||||
|
|
||||||
|
closures = []
|
||||||
|
references = []
|
||||||
|
# Walk just the direct nodes of the test method
|
||||||
|
for child_node in ast.iter_child_nodes(node):
|
||||||
|
if isinstance(child_node, ast.FunctionDef):
|
||||||
|
closures.append(child_node.name)
|
||||||
|
|
||||||
|
# Walk all nodes to find references
|
||||||
|
find_references = _FindVariableReferences()
|
||||||
|
find_references.generic_visit(node)
|
||||||
|
references = find_references._references
|
||||||
|
|
||||||
|
missed = set(closures) - set(references)
|
||||||
|
if missed:
|
||||||
|
self.add_error(node, 'N349: Test closures not called: %s'
|
||||||
|
% ','.join(missed))
|
||||||
|
|
||||||
|
|
||||||
def assert_true_or_false_with_in(logical_line):
|
def assert_true_or_false_with_in(logical_line):
|
||||||
"""Check for assertTrue/False(A in B), assertTrue/False(A not in B),
|
"""Check for assertTrue/False(A in B), assertTrue/False(A not in B),
|
||||||
assertTrue/False(A in B, message) or assertTrue/False(A not in B, message)
|
assertTrue/False(A in B, message) or assertTrue/False(A not in B, message)
|
||||||
@ -732,3 +804,4 @@ def factory(register):
|
|||||||
register(check_python3_no_itervalues)
|
register(check_python3_no_itervalues)
|
||||||
register(cfg_help_with_enough_text)
|
register(cfg_help_with_enough_text)
|
||||||
register(no_os_popen)
|
register(no_os_popen)
|
||||||
|
register(CheckForUncalledTestClosure)
|
||||||
|
@ -750,3 +750,30 @@ class HackingTestCase(test.NoDBTestCase):
|
|||||||
errors = [(4, 0, 'N348'), (8, 8, 'N348')]
|
errors = [(4, 0, 'N348'), (8, 8, 'N348')]
|
||||||
self._assert_has_errors(code, checks.no_os_popen,
|
self._assert_has_errors(code, checks.no_os_popen,
|
||||||
expected_errors=errors)
|
expected_errors=errors)
|
||||||
|
|
||||||
|
def test_uncalled_closures(self):
|
||||||
|
|
||||||
|
checker = checks.CheckForUncalledTestClosure
|
||||||
|
code = """
|
||||||
|
def test_fake_thing():
|
||||||
|
def _test():
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
self._assert_has_errors(code, checker,
|
||||||
|
expected_errors=[(1, 0, 'N349')])
|
||||||
|
|
||||||
|
code = """
|
||||||
|
def test_fake_thing():
|
||||||
|
def _test():
|
||||||
|
pass
|
||||||
|
_test()
|
||||||
|
"""
|
||||||
|
self._assert_has_no_errors(code, checker)
|
||||||
|
|
||||||
|
code = """
|
||||||
|
def test_fake_thing():
|
||||||
|
def _test():
|
||||||
|
pass
|
||||||
|
self.assertRaises(FakeExcepion, _test)
|
||||||
|
"""
|
||||||
|
self._assert_has_no_errors(code, checker)
|
||||||
|
Loading…
Reference in New Issue
Block a user