Permalink
Browse files

Add unit tests. All of them.

- Add unit tests for the main functionality of the site. Trivial
  or features better suited to end-to-end testing were skipped.
- Fix a small issue where users could leave feedback on task attempts
  that did not belong to them.
  • Loading branch information...
1 parent 41519ca commit 5c0d18d78f4fee457066d774668398212dbcd5a7 @Osmose Osmose committed Jan 8, 2014
View
@@ -7,6 +7,7 @@
# Edit this if necessary or override the variable in your environment.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oneanddone.settings')
+os.environ.setdefault('CELERY_LOADER', 'django')
# Add a temporary path so that we can import the funfactory
tmp_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
@@ -1,3 +1,6 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from django.conf import settings
from jingo import register
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from test_utils import TestCase as BaseTestCase
+
+
+class TestCase(BaseTestCase):
+ """Base class for tests across the site."""
+ def shortDescription(self):
+ return None
@@ -0,0 +1,35 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from django.test.utils import override_settings
+
+from mock import patch
+from nose.tools import eq_
+
+from oneanddone.base.helpers import less_css
+from oneanddone.base.tests import TestCase
+
+
+class LessCssTests(TestCase):
+ @override_settings(TEMPLATE_DEBUG=False)
+ def test_template_debug_false_call_css(self):
+ """If TEMPLATE_DEBUG is false, call jingo_minify.helpers.css."""
+ with patch('oneanddone.base.helpers.css') as mock_css:
+ eq_(less_css('bundle'), mock_css.return_value)
+ mock_css.assert_called_with('bundle')
+
+ @override_settings(TEMPLATE_DEBUG=True)
+ def test_template_debug_true_less_tags(self):
+ """
+ If TEMPLATE_DEBUG is true, return a set of link tags with
+ stylesheet/less as the rel attribute.
+ """
+ with patch('oneanddone.base.helpers.get_css_urls') as get_css_urls:
+ get_css_urls.return_value = ['foo', 'bar']
+ output = less_css('bundle')
+ get_css_urls.assert_called_with('bundle')
+
+ self.assertHTMLEqual(output, """
+ <link rel="stylesheet/less" media="screen,projection,tv" href="foo" />
+ <link rel="stylesheet/less" media="screen,projection,tv" href="bar" />
+ """)
@@ -0,0 +1,31 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from mock import Mock
+from nose.tools import eq_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.base.util import get_object_or_none
+
+
+class GetObjectOrNoneTests(TestCase):
+ def test_get(self):
+ """
+ If no exceptions are raised, return the value returned by
+ Model.objects.get.
+ """
+ Model = Mock()
+ eq_(get_object_or_none(Model, foo='bar'), Model.objects.get.return_value)
+ Model.objects.get.assert_called_with(foo='bar')
+
+ def test_none(self):
+ """
+ If a DoesNotExist or MultipleObjectsReturned exception is
+ raised, return None.
+ """
+ Model = Mock(DoesNotExist=Exception, MultipleObjectsReturned=Exception)
+ Model.objects.get.side_effect = Model.DoesNotExist
+ eq_(get_object_or_none(Model, foo='bar'), None)
+
+ Model.objects.get.side_effect = Model.MultipleObjectsReturned
+ eq_(get_object_or_none(Model, foo='bar'), None)
@@ -0,0 +1,35 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from mock import Mock, patch
+from nose.tools import eq_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.base.views import HomeView
+
+
+class HomeViewTests(TestCase):
+ def setUp(self):
+ self.view = HomeView()
+ self.view.request = Mock()
+
+ def test_get_authenticated_redirect(self):
+ """
+ If the current user is authenticated, redirect them to the
+ profile detail view.
+ """
+ self.view.request.user.is_authenticated.return_value = True
+
+ with patch('oneanddone.base.views.redirect') as redirect:
+ eq_(self.view.get(), redirect.return_value)
+ redirect.assert_called_with('users.profile.detail')
+
+ def test_get_unauthenticated_super(self):
+ """
+ If the current user isn't authenticated, call the parent method.
+ """
+ self.view.request.user.is_authenticated.return_value = False
+
+ with patch('oneanddone.base.views.HomeView.get') as parent_get:
+ eq_(self.view.get('baz', foo='bar'), parent_get.return_value)
+ parent_get.assert_called_with('baz', foo='bar')
@@ -0,0 +1,38 @@
+from factory import DjangoModelFactory, fuzzy, Sequence, SubFactory
+
+from oneanddone.tasks import models
+from oneanddone.users.tests import UserFactory
+
+
+class TaskAreaFactory(DjangoModelFactory):
+ FACTORY_FOR = models.TaskArea
+
+ name = Sequence(lambda n: 'test{0}'.format(n))
+ creator = SubFactory(UserFactory)
+
+
+class TaskFactory(DjangoModelFactory):
+ FACTORY_FOR = models.Task
+
+ area = SubFactory(TaskAreaFactory)
+ name = Sequence(lambda n: 'test{0}'.format(n))
+ short_description = Sequence(lambda n: 'test_description{0}'.format(n))
+ instructions = Sequence(lambda n: 'test_instructions{0}'.format(n))
+ execution_time = fuzzy.FuzzyInteger(0, 60)
+ is_draft = False
+ creator = SubFactory(UserFactory)
+
+
+class TaskAttemptFactory(DjangoModelFactory):
+ FACTORY_FOR = models.TaskAttempt
+
+ user = SubFactory(UserFactory)
+ task = SubFactory(TaskFactory)
+ state = models.TaskAttempt.STARTED
+
+
+class FeedbackFactory(DjangoModelFactory):
+ FACTORY_FOR = models.Feedback
+
+ attempt = SubFactory(TaskAttemptFactory)
+ text = Sequence(lambda n: 'feedback{0}'.format(n))
@@ -0,0 +1,44 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from mock import Mock
+from nose.tools import eq_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.tasks.admin import RecordCreatorMixin
+
+
+class FakeModelAdmin(object):
+ def save_model(self, request, obj, form, change):
+ return 'saved'
+
+
+class FakeModelAdminWithMixin(RecordCreatorMixin, FakeModelAdmin):
+ pass
+
+
+class RecordCreatorMixinTests(TestCase):
+ def setUp(self):
+ self.model_admin = FakeModelAdminWithMixin()
+
+ def test_save_model_no_pk(self):
+ """
+ If an object isn't saved yet (has no pk), set the creator to the
+ request's current user.
+ """
+ obj = Mock(pk=None)
+ request = Mock(user='foo')
+
+ self.model_admin.save_model(request, obj, None, False)
+ eq_(obj.creator, request.user)
+
+ def test_save_model_with_pk(self):
+ """
+ If an object exists in the DB (has a pk), do not change the
+ creator.
+ """
+ obj = Mock(pk=5, creator='bar')
+ request = Mock(user='foo')
+
+ self.model_admin.save_model(request, obj, None, False)
+ eq_(obj.creator, 'bar')
@@ -0,0 +1,68 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from nose.tools import eq_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.tasks.filters import AvailableTasksFilterSet, TreeFilter
+from oneanddone.tasks.models import TaskArea
+from oneanddone.tasks.tests import TaskAreaFactory, TaskFactory
+
+
+class TreeFilterTests(TestCase):
+ def test_filter(self):
+ """
+ Filter should match objects that are related to the given value
+ or it's descendants via the field specified in the filter name.
+ """
+ # root -> child1 -> grandchild1 -> great_grandchild1
+ # \-> child2
+ root = TaskAreaFactory.create()
+ child1, child2 = TaskAreaFactory.create_batch(2, parent=root)
+ grandchild1 = TaskAreaFactory.create(parent=child1)
+ great_grandchild1 = TaskAreaFactory.create(parent=grandchild1)
+
+ # Should match all areas "below" child1.
+ tree_filter = TreeFilter(name='parent')
+ areas = tree_filter.filter(TaskArea.objects.all(), child1)
+ eq_(set(areas), set([grandchild1, great_grandchild1]))
+
+
+class AvailableTasksFilterSetTests(TestCase):
+ def test_area_filter_only_with_available_tasks(self):
+ """
+ Only TaskAreas with available tasks and their parents should be
+ included in the area filter.
+ """
+ # root -> child1 -> grandchild1
+ # \-> child2
+ root = TaskAreaFactory.create()
+ child1, child2 = TaskAreaFactory.create_batch(2, parent=root)
+ grandchild1 = TaskAreaFactory.create(parent=child1)
+
+ # Only grandchild1 has available tasks.
+ TaskFactory.create(area=grandchild1, is_draft=False)
+
+ # Area should include grandlchild1 and its parents.
+ filter_set = AvailableTasksFilterSet()
+ areas = filter_set.filters['area'].extra['queryset']
+ eq_(set(areas), set([root, child1, grandchild1]))
+
+ def test_area_filter_empty_children(self):
+ """
+ If a TaskArea has available tasks but its children don't, the
+ children should not be included in the area filter.
+ """
+ # root -> child1 -> grandchild1
+ # \-> child2
+ root = TaskAreaFactory.create()
+ child1, child2 = TaskAreaFactory.create_batch(2, parent=root)
+ grandchild1 = TaskAreaFactory.create(parent=child1)
+
+ # Only child1 has available tasks.
+ TaskFactory.create(area=child1, is_draft=False)
+
+ # Area should include child1, but not grandchild1.
+ filter_set = AvailableTasksFilterSet()
+ areas = filter_set.filters['area'].extra['queryset']
+ eq_(set(areas), set([root, child1]))
@@ -0,0 +1,33 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import urlparse
+
+from mock import Mock
+from nose.tools import eq_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.tasks.helpers import page_url
+
+
+class PageUrlTests(TestCase):
+ def test_basic(self):
+ """
+ page_url should return a relative link to the current page,
+ preserving the GET arguments from the given request, and adding
+ a page parameter for the given page.
+ """
+ request = Mock(GET={'foo': 'bar', 'baz': 5})
+ url = urlparse.urlsplit(page_url(request, 4))
+ args = urlparse.parse_qs(url.query)
+ eq_(args, {'foo': ['bar'], 'baz': ['5'], 'page': ['4']})
+
+ def test_existing_page_arg(self):
+ """
+ If the current page already has a page GET argument, override
+ it.
+ """
+ request = Mock(GET={'foo': 'bar', 'page': 5})
+ url = urlparse.urlsplit(page_url(request, 4))
+ args = urlparse.parse_qs(url.query)
+ eq_(args, {'foo': ['bar'], 'page': ['4']})
@@ -0,0 +1,37 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from mock import Mock, patch
+from nose.tools import eq_
+
+from oneanddone.base.tests import TestCase
+from oneanddone.tasks import mixins
+
+
+class TaskMustBePublishedMixinTests(TestCase):
+ def make_view(self, queryset, allow_expired_tasks_attr):
+ """
+ Create a fake view that applies the mixin to the given queryset
+ when get_queryset is called.
+ """
+ class BaseView(object):
+ def get_queryset(self):
+ return queryset
+
+ class View(mixins.TaskMustBePublishedMixin, BaseView):
+ allow_expired_tasks = allow_expired_tasks_attr
+
+ return View()
+
+ def test_get_queryset(self):
+ """
+ get_queryset should filter the parent class' queryset with the
+ availability filter from Task.
+ """
+ queryset = Mock()
+ view = self.make_view(queryset, False)
+
+ with patch('oneanddone.tasks.mixins.Task') as Task:
+ eq_(view.get_queryset(), queryset.filter.return_value)
+ queryset.filter.assert_called_with(Task.is_available_filter.return_value)
+ Task.is_available_filter.assert_called_with(allow_expired=False)
Oops, something went wrong.

0 comments on commit 5c0d18d

Please sign in to comment.