Skip to content

Commit

Permalink
feat(test): add test setup using py.test
Browse files Browse the repository at this point in the history
- refractor settings to be top-level module.
- make junction as top level module, changes imports to be more explicit
- add py.test fixtures - client
- divide the tests into `integration` and `unit` with corresponding folders
- add basic url integration test.

Breaking:
- location of wsgi is changed from `junction.junction.wsgi` to `wsgi`,
  this should be taken care while deployment in production.
- `manage.py` lives on the top-level, instead of i level deep.
  • Loading branch information
Saurabh Kumar committed Jan 6, 2015
1 parent 48a66f4 commit 351073a
Show file tree
Hide file tree
Showing 28 changed files with 260 additions and 60 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ install:

script:
- flake8
- coverage run --source=junction --omit='*tests*,*commands*,*migrations*,*admin*,*wsgi*' -m py.test -v --tb=native
- coverage report

notifications:
email:
Expand Down
File renamed without changes.
12 changes: 6 additions & 6 deletions junction/conferences/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from django.contrib import admin

from conferences.models import Conference, ConferenceModerator, \
ConferenceProposalReviewer
from custom_utils.admin import AuditAdmin
from junction.custom_utils.admin import AuditAdmin

from . import models


class ConferenceAdmin(AuditAdmin):
Expand All @@ -17,6 +17,6 @@ class ConferenceProposallReviewerAdmin(AuditAdmin):
list_display = ('conference', 'reviewer', 'active') + AuditAdmin.list_display


admin.site.register(Conference, ConferenceAdmin)
admin.site.register(ConferenceModerator, ConferenceModeratorAdmin)
admin.site.register(ConferenceProposalReviewer, ConferenceProposallReviewerAdmin)
admin.site.register(models.Conference, ConferenceAdmin)
admin.site.register(models.ConferenceModerator, ConferenceModeratorAdmin)
admin.site.register(models.ConferenceProposalReviewer, ConferenceProposallReviewerAdmin)
4 changes: 2 additions & 2 deletions junction/conferences/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from django.db import models
from django_extensions.db.fields import AutoSlugField

from custom_utils.constants import CONFERENCE_STATUS_LIST
from custom_utils.models import AuditModel
from junction.custom_utils.constants import CONFERENCE_STATUS_LIST
from junction.custom_utils.models import AuditModel


class Conference(AuditModel):
Expand Down
13 changes: 0 additions & 13 deletions junction/junction/dev.py

This file was deleted.

2 changes: 1 addition & 1 deletion junction/pages/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Third Party Stuff
from django.views.generic import TemplateView
from conferences.models import Conference
from junction.conferences.models import Conference


class HomePageView(TemplateView):
Expand Down
4 changes: 2 additions & 2 deletions junction/proposals/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.contrib import admin

from custom_utils.admin import AuditAdmin, TimeAuditAdmin
from proposals.models import (
from junction.custom_utils.admin import AuditAdmin, TimeAuditAdmin
from junction.proposals.models import (
Proposal,
ProposalComment,
ProposalCommentVote,
Expand Down
2 changes: 1 addition & 1 deletion junction/proposals/comment_urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.conf.urls import patterns, url

from proposals.views import create_proposal_comment
from .views import create_proposal_comment


urlpatterns = patterns(
Expand Down
4 changes: 2 additions & 2 deletions junction/proposals/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from django.utils.safestring import mark_safe
from pagedown.widgets import PagedownWidget

from custom_utils.constants import PROPOSAL_TARGET_AUDIENCES, PROPOSAL_STATUS_LIST
from proposals.models import ProposalSection, ProposalType
from junction.custom_utils.constants import PROPOSAL_TARGET_AUDIENCES, PROPOSAL_STATUS_LIST
from junction.proposals.models import ProposalSection, ProposalType


def _get_proposal_section_choices(conference):
Expand Down
14 changes: 10 additions & 4 deletions junction/proposals/models.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

# Third Party Stuff
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.db import models

from django_extensions.db.fields import AutoSlugField

from conferences.models import Conference
from custom_utils.constants import PROPOSAL_USER_VOTE_ROLES, PROPOSAL_STATUS_LIST, \
PROPOSAL_REVIEW_STATUS_LIST, PROPOSAL_TARGET_AUDIENCES
from custom_utils.models import AuditModel, TimeAuditModel
from junction.conferences.models import Conference
from junction.custom_utils.constants import (
PROPOSAL_REVIEW_STATUS_LIST,
PROPOSAL_STATUS_LIST,
PROPOSAL_TARGET_AUDIENCES,
PROPOSAL_USER_VOTE_ROLES
)
from junction.custom_utils.models import AuditModel, TimeAuditModel


class ProposalSection(AuditModel):
Expand Down
12 changes: 10 additions & 2 deletions junction/proposals/proposal_urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
from django.conf.urls import patterns, url
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from proposals.views import create_proposal, list_proposals, update_proposal, detail_proposal, delete_proposal
from django.conf.urls import patterns, url

from .views import (
create_proposal,
delete_proposal,
detail_proposal,
list_proposals,
update_proposal
)

urlpatterns = patterns(
'',
Expand Down
10 changes: 7 additions & 3 deletions junction/proposals/views.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

# Third Party Stuff
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
from django.http.response import HttpResponseForbidden, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.views.decorators.http import require_http_methods

from conferences.models import Conference, ConferenceProposalReviewer
from junction.conferences.models import Conference, ConferenceProposalReviewer

from proposals.forms import ProposalCommentForm, ProposalForm, ProposalVoteForm
from proposals.models import Proposal, ProposalComment, ProposalVote
from .forms import ProposalCommentForm, ProposalForm, ProposalVoteForm
from .models import Proposal, ProposalComment, ProposalVote


def _is_proposal_author(user, proposal):
Expand Down
2 changes: 1 addition & 1 deletion junction/proposals/vote_urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.conf.urls import patterns, url

from proposals.views import proposal_vote_up, proposal_vote_down
from .views import proposal_vote_up, proposal_vote_down


urlpatterns = patterns(
Expand Down
8 changes: 4 additions & 4 deletions junction/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
url('^markdown/', include('django_markdown.urls')),

# Proposals related
url(r'^(?P<conference_slug>[\w-]+)/proposals/', include('proposals.proposal_urls')),
url(r'^(?P<conference_slug>[\w-]+)/proposal-comments/', include('proposals.comment_urls')),
url(r'^(?P<conference_slug>[\w-]+)/proposal-votes/', include('proposals.vote_urls')),
url(r'^(?P<conference_slug>[\w-]+)/proposals/', include('junction.proposals.proposal_urls')),
url(r'^(?P<conference_slug>[\w-]+)/proposal-comments/', include('junction.proposals.comment_urls')),
url(r'^(?P<conference_slug>[\w-]+)/proposal-votes/', include('junction.proposals.vote_urls')),

# Static Pages. TODO: to be refactored
url(r'^speakers/$', TemplateView.as_view(template_name='static-content/speakers.html',), name='speakers-static'),
Expand All @@ -42,7 +42,7 @@
name='conference-detail'),

# add at the last for minor performance gain
url(r'^', include('pages.urls', namespace='pages')),
url(r'^', include('junction.pages.urls', namespace='pages')),
)

if settings.DEBUG:
Expand Down
2 changes: 1 addition & 1 deletion junction/manage.py → manage.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys

if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "junction.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")

from django.core.management import execute_from_command_line

Expand Down
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[pytest]
DJANGO_SETTINGS_MODULE = settings
python_paths = .
norecursedirs = .tox .git */migrations/* */static/* docs venv
8 changes: 8 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,12 @@

# Testing
# -------------------------------------------------
mock==1.0.1
factory_boy==2.4.1
flake8==2.2.5
pytest-django==2.7.0
pytest-flakes==0.2
pytest-pythonpath==0.3
pytest-ipdb==0.1-prerelease2

coverage==3.7.1
18 changes: 18 additions & 0 deletions settings/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import print_function

# Standard Library
import sys

if "test" in sys.argv:
print("\033[1;91mNo django tests.\033[0m")
print("Try: \033[1;33mpy.test\033[0m")
sys.exit(0)

from .common import * # noqa

try:
from .dev import * # noqa
from .prod import * # noqa
except ImportError:
pass
31 changes: 14 additions & 17 deletions junction/junction/settings.py → settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

from django.conf.global_settings import * # noqa

BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Standard Library
from os.path import dirname, join

# Build paths inside the project like this: os.path.join(ROOT_DIR, ...)
ROOT_DIR = dirname(dirname(__file__))
APP_DIR = join(ROOT_DIR, 'junction')

SITE_ID = 1

Expand Down Expand Up @@ -44,9 +49,9 @@
)

OUR_APPS = (
'conferences',
'proposals',
'pages',
'junction.conferences',
'junction.proposals',
'junction.pages',
)

INSTALLED_APPS = CORE_APPS + THIRD_PARTY_APPS + OUR_APPS
Expand Down Expand Up @@ -125,21 +130,21 @@
}


ROOT_URLCONF = 'urls'
WSGI_APPLICATION = 'junction.wsgi.application'
ROOT_URLCONF = 'junction.urls'
WSGI_APPLICATION = 'wsgi.application'

TIME_ZONE = 'Asia/Kolkata'
USE_L10N = True
USE_TZ = True

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'assets', 'collected-static')
STATIC_ROOT = os.path.join(APP_DIR, 'assets', 'collected-static')
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'assets', 'static'),
os.path.join(APP_DIR, 'assets', 'static'),
)

TEMPLATE_DIRS = (
os.path.join(BASE_DIR, 'templates'),
os.path.join(APP_DIR, 'templates'),
)

DATABASES = {
Expand All @@ -165,11 +170,3 @@
DEBUG = TEMPLATE_DEBUG = os.environ.get('DEBUG', 'on') == 'on'

ALLOWED_HOSTS = [] # TODO:

# Dev Settings

try:
from junction.dev import * # noqa
from junction.prod import * # noqa
except ImportError:
pass
File renamed without changes.
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
15 changes: 15 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Standard Library
import os

# Third Party Stuff
import django
# import pytest

from .fixtures import * # noqa

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")


def pytest_configure(config):
django.setup()
53 changes: 53 additions & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# Standard Library
import functools

# Third Party Stuff
import mock
import pytest


class Object:
pass


@pytest.fixture
def object():
return Object()


class PartialMethodCaller:
def __init__(self, obj, **partial_params):
self.obj = obj
self.partial_params = partial_params

def __getattr__(self, name):
return functools.partial(getattr(self.obj, name), **self.partial_params)


@pytest.fixture
def client():
from django.test.client import Client

class _Client(Client):
def login(self, user=None, backend="django.contrib.auth.backends.ModelBackend", **credentials):
if user is None:
return super(_Client, self).login(**credentials)

with mock.patch('django.contrib.auth.authenticate') as authenticate:
user.backend = backend
authenticate.return_value = user
return super(_Client, self).login(**credentials)

@property
def json(self):
return PartialMethodCaller(obj=self, content_type='application/json;charset="utf-8"')

return _Client()


@pytest.fixture
def outbox():
from django.core import mail

return mail.outbox
1 change: 1 addition & 0 deletions tests/integrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
17 changes: 17 additions & 0 deletions tests/integrations/test_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pytest
from django.core.urlresolvers import reverse


pytestmark = pytest.mark.django_db


def test_public_urls(client):

public_urls = [
reverse('pages:homepage'),
'/nimda/login/',
]

for url in public_urls:
response = client.get(url)
assert response.status_code == 200
1 change: 1 addition & 0 deletions tests/unit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
Loading

0 comments on commit 351073a

Please sign in to comment.