Skip to content

Add initial approach to put Django's asserts into pytest's namespace. Issue #97 (was: Pull Request #144) #232

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 88 additions & 4 deletions pytest_django/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
This plugin handles creating and destroying the test environment and
test database and provides some useful text fixtures.
"""

import contextlib
import inspect
from functools import reduce
Expand Down Expand Up @@ -40,10 +39,96 @@
SETTINGS_MODULE_ENV = 'DJANGO_SETTINGS_MODULE'
CONFIGURATION_ENV = 'DJANGO_CONFIGURATION'
INVALID_TEMPLATE_VARS_ENV = 'FAIL_INVALID_TEMPLATE_VARS'

DJANGO_ASSERTS = {
# from https://github.com/django/django/blob/stable/1.3.x/django/test/testcases.py#L244
(1, 3): ('assertContains', 'assertFormError', 'assertNotContains',
'assertNumQueries', 'assertQuerysetEqual', 'assertRedirects',
'assertTemplateNotUsed', 'assertTemplateUsed'),
# from https://github.com/django/django/blob/stable/1.4.x/django/test/testcases.py#L448
(1, 4): ('assertContains', 'assertFieldOutput', 'assertFormError',
'assertHTMLEqual', 'assertHTMLNotEqual', 'assertNotContains',
'assertNumQueries', 'assertQuerysetEqual', 'assertRaisesMessage',
'assertRedirects', 'assertTemplateNotUsed', 'assertTemplateUsed'),
# from https://github.com/django/django/blob/stable/1.5.x/django/test/testcases.py#L246
# and https://github.com/django/django/blob/stable/1.5.x/django/test/testcases.py#L458
(1, 5): ('assertContains', 'assertFieldOutput', 'assertFormError',
'assertHTMLEqual', 'assertHTMLNotEqual', 'assertInHTML',
'assertJSONEqual', 'assertNotContains', 'assertNumQueries',
'assertQuerysetEqual', 'assertRaisesMessage', 'assertRedirects',
'assertTemplateNotUsed', 'assertTemplateUsed', 'assertXMLEqual',
'assertXMLNotEqual'),
# from https://github.com/django/django/blob/stable/1.6.x/django/test/testcases.py#L156
# and https://github.com/django/django/blob/stable/1.6.x/django/test/testcases.py#L725
(1, 6): ('assertContains', 'assertFieldOutput', 'assertFormError',
'assertFormsetError', 'assertHTMLEqual', 'assertHTMLNotEqual',
'assertInHTML', 'assertJSONEqual', 'assertNotContains',
'assertNumQueries', 'assertQuerysetEqual', 'assertRaisesMessage',
'assertRedirects', 'assertTemplateNotUsed', 'assertTemplateUsed',
'assertXMLEqual', 'assertXMLNotEqual'),
# from https://github.com/django/django/blob/stable/1.7.x/django/test/testcases.py#L162
# and https://github.com/django/django/blob/stable/1.7.x/django/test/testcases.py#L722
(1, 7): ('assertContains', 'assertFieldOutput', 'assertFormError',
'assertFormsetError', 'assertHTMLEqual', 'assertHTMLNotEqual',
'assertInHTML', 'assertJSONEqual', 'assertNotContains',
'assertNumQueries', 'assertQuerysetEqual', 'assertRaisesMessage',
'assertRedirects', 'assertTemplateNotUsed', 'assertTemplateUsed',
'assertXMLEqual', 'assertXMLNotEqual'),
# from https://github.com/django/django/blob/stable/1.8.x/django/test/testcases.py#L142
# and https://github.com/django/django/blob/stable/1.8.x/django/test/testcases.py#L751
(1, 8): ('assertContains', 'assertFieldOutput', 'assertFormError',
'assertFormsetError', 'assertHTMLEqual', 'assertHTMLNotEqual',
'assertInHTML', 'assertJSONEqual', 'assertJSONNotEqual',
'assertNotContains', 'assertNumQueries', 'assertQuerysetEqual',
'assertRaisesMessage', 'assertRedirects', 'assertTemplateNotUsed',
'assertTemplateUsed', 'assertXMLEqual', 'assertXMLNotEqual'),
# from https://github.com/django/django/blob/stable/1.9.x/django/test/testcases.py#L158
# and https://github.com/django/django/blob/stable/1.9.x/django/test/testcases.py#L818
(1, 9): ('assertContains', 'assertFieldOutput', 'assertFormError',
'assertFormsetError', 'assertHTMLEqual', 'assertHTMLNotEqual',
'assertInHTML', 'assertJSONEqual', 'assertJSONNotEqual',
'assertNotContains', 'assertNumQueries', 'assertQuerysetEqual',
'assertRaisesMessage', 'assertRedirects', 'assertTemplateNotUsed',
'assertTemplateUsed', 'assertXMLEqual', 'assertXMLNotEqual'),
# from https://github.com/django/django/blob/stable/1.10.x/django/test/testcases.py#L155
# and https://github.com/django/django/blob/stable/1.10.x/django/test/testcases.py#L797
(1, 10): ('assertContains', 'assertFieldOutput', 'assertFormError',
'assertFormsetError', 'assertHTMLEqual', 'assertHTMLNotEqual',
'assertInHTML', 'assertJSONEqual', 'assertJSONNotEqual',
'assertNotContains', 'assertNumQueries', 'assertQuerysetEqual',
'assertRaisesMessage', 'assertRedirects', 'assertTemplateNotUsed',
'assertTemplateUsed', 'assertXMLEqual', 'assertXMLNotEqual'),
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of the sets are equal, maybe it could be moved into some constant?!
I.e. one for Django 1.8 and than use it for 1.9 and 1.10 also?!

Copy link

@das-g das-g Jan 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the duplication here is not exactly accidental, I think it's save to say that it's merely coincidental. There is most probably no deep underlying principle behind what functions are shared between several versions and which aren't.

Thus, extracting the common function names to eliminate duplication might make this configuration less clear and maintainable instead of more so. (cf. Beware the Share by Udi Dahan

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment, they can be formed dynamically rather than using any duplication in the file or from django itself.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These can be extracted dyanmically with e.g

In [3]: {attr for attr in SimpleTestCase.__dict__ if attr.startswith('assert')}
Out[3]:
{'assertContains',
 'assertFieldOutput',
 'assertFormError',
 'assertFormsetError',
 'assertHTMLEqual',
 'assertHTMLNotEqual',
 'assertInHTML',
 'assertJSONEqual',
 'assertJSONNotEqual',
 'assertNotContains',
 'assertRaisesMessage',
 'assertRedirects',
 'assertTemplateNotUsed',
 'assertTemplateUsed',
 'assertXMLEqual',
 'assertXMLNotEqual'}

This works because a class's __dict__ contains the names defined in it, but not looking up its MRO.

You probably need to look at TestCase and TransactionTestCase too to get them all, didn't check that.


# ############### pytest hooks ################

def populate_namespace():
def _wrapper(name):

def assertion_func(*args, **kwargs):
from django.test import TestCase as DjangoTestCase

getattr(DjangoTestCase('run'), name)(*args, **kwargs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does ('run') do in this case? Just trying to understand why it's necessary


return assertion_func

asserts = {}
for name in DJANGO_ASSERTS[get_django_version()[:2]]:
asserts[name] = _wrapper(name)

return {'django': asserts}


def pytest_namespace():
"""Make unittest assert methods available.
Useful for things such as floating point checks with assertAlmostEqual.
"""
try:
django_settings_is_configured()
except AssertionError:
return populate_namespace()
return {}


def pytest_addoption(parser):
group = parser.getgroup('django')
group._addoption('--reuse-db',
Expand Down Expand Up @@ -594,8 +679,7 @@ def _save_active_wrapper(self):
return self._history.append(self._dj_db_wrapper.ensure_connection)

def _blocking_wrapper(*args, **kwargs):
__tracebackhide__ = True
__tracebackhide__ # Silence pyflakes
__tracebackhide__ = True # noqa
pytest.fail('Database access not allowed, '
'use the "django_db" mark, or the '
'"db" or "transactional_db" fixtures to enable it.')
Expand Down
44 changes: 44 additions & 0 deletions tests/test_namespaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# coding: utf-8
import inspect

import pytest
import pytest_django

from pytest_django.lazy_django import get_django_version
from pytest_django.plugin import DJANGO_ASSERTS


def _get_actual_assertions_names():
"""
Returns list with names of all assertion helpers in Django.
"""
from django.test import TestCase as DjangoTestCase
from django.utils.unittest import TestCase as DefaultTestCase

obj = DjangoTestCase('run')
is_assert = lambda x: x.startswith('assert') and '_' not in x
base_methods = [name for name, member in
inspect.getmembers(DefaultTestCase)
if is_assert(name)]

return [name for name, member in inspect.getmembers(obj)
if is_assert(name) and name not in base_methods]


def test_django_asserts_available():
django_assertions = _get_actual_assertions_names()
expected_assertions = DJANGO_ASSERTS[get_django_version()[:2]]
assert set(django_assertions) == set(expected_assertions)

for name in expected_assertions:
assert hasattr(pytest_django, name)


def test_sanity(admin_client):
from pytest_django import assertContains

response = admin_client.get('/admin-required/')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test is necessarily slow.
A faster functionality should be tested here.


assertContains(response, 'You are an admin')
with pytest.raises(AssertionError):
assertContains(response, 'Access denied')