Skip to content

Commit

Permalink
Merge pull request #35 from jamescooke/unittest-support
Browse files Browse the repository at this point in the history
Initial Unittest support
  • Loading branch information
jamescooke committed Jul 14, 2018
2 parents 47294ab + c5a05a2 commit ec95894
Show file tree
Hide file tree
Showing 15 changed files with 213 additions and 21 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ Unreleased_
See also `latest documentation
<https://flake8-aaa.readthedocs.io/en/latest/>`_.

Added
-----

* Support for unittest tests.

Changed
-------

Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ tox:
.PHONY: lint
lint:
@echo "=== flake8 ==="
flake8 $(lint_files)
flake8 $(lint_files) examples
@echo "=== pylint ==="
./run_pylint.sh
@echo "=== isort ==="
Expand All @@ -33,6 +33,10 @@ lint:
@echo "=== setup.py ==="
python setup.py check --metadata --strict

.PHONY: test
test:
pytest tests


.PHONY: fixlint
fixlint:
Expand Down
4 changes: 3 additions & 1 deletion docs/discovery.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Test discovery
* Filename must start with ``test_`` and have been collected for linting by
``flake8``.

* Test must be a function where its name starts with ``test``.
* Test must be a function or class method.

* Test names must start with ``test``.

* Tests that contain only comments, docstrings or ``pass`` are skipped.
17 changes: 11 additions & 6 deletions docs/rules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@ for Python developers
AAA01: no Act block found in test
---------------------------------

Test found to have no Act block.

An Act block is usually a line like ``result =`` or a check that an exception
is raised using ``with pytest.raises(Exception):``.
is raised. ``flake8-aaa`` could not find an Act block in the indicated test
function.

Resolution
..........

Add an Act block to the test or mark a line that should be considered the
action. Even if the result of a test action is ``None``, assign that result and
action.

Even if the result of a test action is ``None``, assign that result and
pin it with a test::

result = action()
Expand All @@ -29,6 +30,9 @@ the Act block with ``# act`` (case insensitive)::

data['new_key'] = 1 # act

Code blocks wrapped in ``pytest.raises()`` and ``unittest.assertRaises()``
context managers are recognised as Act blocks.

AAA02: multiple Act blocks found in test
----------------------------------------

Expand All @@ -40,8 +44,9 @@ act``. Multiple Act blocks create ambiguity and raise this error code.
Resolution
..........

Splitting the failing test into multiple tests. Where there is complicated or
reused set-up code then that should be extracted into one or more fixtures.
Split the failing test into multiple tests. Where there is complicated or
reused set-up code then apply the DRY principle and extract the reused code
into one or more fixtures.

AAA03: expected 1 blank line before Act block, found none
---------------------------------------------------------
Expand Down
36 changes: 36 additions & 0 deletions examples/good/test_account_edit_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse


class TestAccountEditView(TestCase):

def setUp(self):
User = get_user_model()
self.user = User(
email='test@example.com',
is_active=1
)
self.user.set_password('blah')
self.user.save()

def test_url_resolves(self):
result = reverse('account.edit')

self.assertEqual(result, '/account/')

def test_not_logged_in(self):
url = reverse('account.edit')

result = self.client.get(url)

self.assertEqual(result.status_code, 302)

def test_loads_template(self):
url = reverse('account.edit')
self.client.login(email='test@example.com', password='blah')

result = self.client.get(url)

self.assertEqual(result.status_code, 200)
self.assertTemplateUsed(result, 'account/account.html')
48 changes: 48 additions & 0 deletions examples/good/test_django_fakery_factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.utils import IntegrityError
from django.test import TestCase

from ..django_fakery_factories import ItemFactory, UserFactory
from ..models import Item


class TestItemFactory(TestCase):

def test_default(self):
"""
Django Fakery: Plant.Item: RED (creates invalid items)
"""
result = ItemFactory()

self.assertEqual(Item.objects.count(), 1)
with self.assertRaises(ValidationError) as cm:
result.full_clean()
self.assertIn('has at most 1 character', str(cm.exception))


class TestUserFactory(TestCase):

user_model = get_user_model()

def test_default(self):
"""
Django Fakery: User Model: YELLOW (raises IntegrityError)
We have to push the number of Users created, but since Django Fakery
has no collision protection and uses a small number of latin words it
will always fail sooner or later. Sometimes on the second user.
However, instances created are valid if they are able to enter the
database.
"""
with self.assertRaises(IntegrityError) as cm:
for expected_num_created in range(1, 100):
with transaction.atomic():
UserFactory()

self.assertEqual(self.user_model.objects.count(), expected_num_created - 1)
self.assertIn('unique', str(cm.exception).lower())
for u in self.user_model.objects.all():
u.full_clean()
4 changes: 3 additions & 1 deletion flake8_aaa/act_block.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ast

from .helpers import node_is_pytest_raises, node_is_result_assignment
from .helpers import node_is_pytest_raises, node_is_result_assignment, node_is_unittest_raises
from .types import ActBlockType


Expand Down Expand Up @@ -47,6 +47,8 @@ def build(cls, node):
return [cls(node, ActBlockType.result_assignment)]
elif node_is_pytest_raises(node):
return [cls(node, ActBlockType.pytest_raises)]
elif node_is_unittest_raises(node):
return [cls(node, ActBlockType.unittest_raises)]

# Check if line marked with '# act'
if node.first_token.line.strip().endswith('# act'):
Expand Down
9 changes: 5 additions & 4 deletions flake8_aaa/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,11 @@ def load_act_block(self):
if len(act_blocks) < 1:
raise ValidationError(self.node.lineno, self.node.col_offset, 'AAA01 no Act block found in test')

if len(act_blocks) > 1:
# Allow `pytest.raises` in assert blocks - if any of the additional
# act blocks are `pytest.raises` blocks, then raise
if list(filter(lambda ab: ab.block_type != ActBlockType.pytest_raises, act_blocks[1:])):
# Allow `pytest.raises` and `self.assertRaises()` in assert blocks - if
# any of the additional act blocks are `pytest.raises` blocks, then
# raise
for a_b in act_blocks[1:]:
if a_b.block_type in [ActBlockType.marked_act, ActBlockType.result_assignment]:
raise ValidationError(self.node.lineno, self.node.col_offset, 'AAA02 multiple Act blocks found in test')

return act_blocks[0]
Expand Down
12 changes: 12 additions & 0 deletions flake8_aaa/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@ def node_is_pytest_raises(node):
return isinstance(node, ast.With) and node.first_token.line.strip().startswith('with pytest.raises')


def node_is_unittest_raises(node):
"""
Args:
node: An ``ast`` node, augmented with ASTTokens
Returns:
bool: ``node`` corresponds to a With node where the context manager is
unittest's ``self.assertRaises``.
"""
return isinstance(node, ast.With) and node.first_token.line.strip().startswith('with self.assertRaises')


def node_is_noop(node):
"""
Args:
Expand Down
5 changes: 4 additions & 1 deletion flake8_aaa/types.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from enum import Enum

ActBlockType = Enum('ActBlockType', 'marked_act pytest_raises result_assignment') # pylint: disable=invalid-name
ActBlockType = Enum( # pylint: disable=invalid-name
'ActBlockType',
'marked_act pytest_raises result_assignment unittest_raises',
)
4 changes: 2 additions & 2 deletions run_pylint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ ERROR_MESSAGE=2
CONVENTION_MESSAGE=16
USAGE_ERROR=32

echo "> Raising on FATAL_MESSAGE, ERROR_MESSAGE, CONVENTION_MESSAGE, USAGE_ERROR"

set +e
pylint flake8_aaa
lint_code=$?
set -e

(((lint_code&FATAL_MESSAGE)>0 || (lint_code&ERROR_MESSAGE)>0 || (lint_code&USAGE_ERROR)>0 || (lint_code&CONVENTION_MESSAGE)>0)) && exit $lint_code

echo "Ignoring WARNING_MESSAGE, REFACTOR_MESSAGE"

exit 0
21 changes: 20 additions & 1 deletion tests/act_block/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def test_not_actions(first_node_with_tokens):
"""
]
)
def test_raises_block(first_node_with_tokens):
def test_pytest_raises_block(first_node_with_tokens):
result = ActBlock.build(first_node_with_tokens.body[0])

assert isinstance(result, list)
Expand All @@ -26,6 +26,25 @@ def test_raises_block(first_node_with_tokens):
assert result[0].block_type == ActBlockType.pytest_raises


@pytest.mark.parametrize(
'code_str', [
"""
def test_not_actions(self):
with self.assertRaises(ValidationError):
self.serializer.is_valid(raise_exception=True)
"""
]
)
def test_unittest_raises_block(first_node_with_tokens):
result = ActBlock.build(first_node_with_tokens.body[0])

assert isinstance(result, list)
assert len(result) == 1
assert isinstance(result[0], ActBlock)
assert result[0].node == first_node_with_tokens.body[0]
assert result[0].block_type == ActBlockType.unittest_raises


@pytest.mark.parametrize(
'code_str, expected_type', [
('result = do_thing()', ActBlockType.result_assignment),
Expand Down
19 changes: 16 additions & 3 deletions tests/function/test_load_act_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def test_act_marker(function):


@pytest.mark.parametrize(
'code_str', [
'code_str',
[
'''
def test(existing_user):
result = existing_user.delete()
Expand All @@ -43,8 +44,20 @@ def test(existing_user):
assert result.retrieved is False
with pytest.raises(DoesNotExist):
result.retrieve()
'''
]
''',
'''
def test(self):
existing_user = self.get_user()
result = existing_user.delete()
self.assertIs(result, True)
self.assertIs(result.retrieved, False)
with self.assertRaises(DoesNotExist):
result.retrieve()
''',
],
ids=['pytest raises in Assert', 'unittest raises in Assert'],
)
def test_raises_in_assert(function):
result = function.load_act_block()
Expand Down
42 changes: 42 additions & 0 deletions tests/helpers/test_node_is_unittest_raises.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import ast

import asttokens
import pytest

from flake8_aaa.helpers import node_is_unittest_raises


@pytest.mark.parametrize(
'code_str', [
'''
def test(self):
with self.assertRaises(IntegrityError) as cm:
do_thing()
''',
'''
def test_other(self):
with self.assertRaises(ValueError) as cm:
do_thing()
''',
]
)
def test(first_node_with_tokens):
with_node = first_node_with_tokens.body[0]

result = node_is_unittest_raises(with_node)

assert result is True


@pytest.mark.parametrize('code_str', [
'''with open('test.txt') as f:
f.read()''',
])
def test_no(code_str):
tree = ast.parse(code_str)
asttokens.ASTTokens(code_str, tree=tree)
node = tree.body[0]

result = node_is_unittest_raises(node)

assert result is False
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ deps =
commands =
install: flake8 --version
install: flake8 tests
test: pytest
test: pytest tests
lint: make lint
setenv = IN_TOX = 1
whitelist_externals = make

0 comments on commit ec95894

Please sign in to comment.