Repository: e-valuation/EvaP

Issue:
Remove ViewTest where possible
Right now we have a `class ViewTest`, for which there is one subclass for each view that we have.

For views that we have tested properly, it provides no additional value and I I propose to replace it with the original `WebTest`. 

Originally I proposed to remove it altogether and copypaste its test to all the test cases that wouldn't have any valuable test otherwise. @janno42 convinced me to leave it there and rename it to `WebTestWith200Check` instead.


Discussion:

Resolving patch:
diff --git a/evap/evaluation/migrations/0002_initial_data.py b/evap/evaluation/migrations/0002_initial_data.py
--- a/evap/evaluation/migrations/0002_initial_data.py
+++ b/evap/evaluation/migrations/0002_initial_data.py
@@ -1,5 +1,4 @@
 from django.db import migrations
-from django.contrib.auth.models import Group
 
 
 def insert_emailtemplates(apps, _schema_editor):
@@ -16,6 +15,7 @@ def insert_emailtemplates(apps, _schema_editor):
         if not EmailTemplate.objects.filter(name=name).exists():
             EmailTemplate.objects.create(name=name, subject=subject, body="")
 
+    Group = apps.get_model("auth", "Group")
     Group.objects.create(name="Staff")
 
 
diff --git a/evap/evaluation/migrations/0055_reviewer_group.py b/evap/evaluation/migrations/0055_reviewer_group.py
--- a/evap/evaluation/migrations/0055_reviewer_group.py
+++ b/evap/evaluation/migrations/0055_reviewer_group.py
@@ -1,12 +1,13 @@
-from django.contrib.auth.models import Group
 from django.db import migrations
 
 
-def add_group(_apps, _schema_editor):
+def add_group(apps, _schema_editor):
+    Group = apps.get_model("auth", "Group")
     Group.objects.create(name="Reviewer")
 
 
-def delete_group(_apps, _schema_editor):
+def delete_group(apps, _schema_editor):
+    Group = apps.get_model("auth", "Group")
     Group.objects.get(name="Reviewer").delete()
 
 
diff --git a/evap/grades/migrations/0002_initial_data.py b/evap/grades/migrations/0002_initial_data.py
--- a/evap/grades/migrations/0002_initial_data.py
+++ b/evap/grades/migrations/0002_initial_data.py
@@ -1,8 +1,8 @@
 from django.db import migrations
-from django.contrib.auth.models import Group
 
 
-def add_group(_apps, _schema_editor):
+def add_group(apps, _schema_editor):
+    Group = apps.get_model("auth", "Group")
     Group.objects.create(name="Grade publisher")
 
 


Test patch:
diff --git a/evap/contributor/tests/test_views.py b/evap/contributor/tests/test_views.py
--- a/evap/contributor/tests/test_views.py
+++ b/evap/contributor/tests/test_views.py
@@ -1,9 +1,10 @@
 from django.core import mail
 
+from django_webtest import WebTest
 from model_mommy import mommy
 
 from evap.evaluation.models import Course, UserProfile, Contribution
-from evap.evaluation.tests.tools import WebTest, ViewTest, create_course_with_responsible_and_editor
+from evap.evaluation.tests.tools import WebTestWith200Check, create_course_with_responsible_and_editor
 
 TESTING_COURSE_ID = 2
 
@@ -55,7 +56,7 @@ def test_direct_delegation_request_with_existing_contribution(self):
         self.assertEqual(len(mail.outbox), 1)
 
 
-class TestContributorView(ViewTest):
+class TestContributorView(WebTestWith200Check):
     url = '/contributor/'
     test_users = ['editor', 'responsible']
 
@@ -64,9 +65,8 @@ def setUpTestData(cls):
         create_course_with_responsible_and_editor()
 
 
-class TestContributorSettingsView(ViewTest):
+class TestContributorSettingsView(WebTest):
     url = '/contributor/settings'
-    test_users = ['editor', 'responsible']
 
     @classmethod
     def setUpTestData(cls):
@@ -82,7 +82,7 @@ def test_save_settings(self):
         self.assertEqual(list(UserProfile.objects.get(username='responsible').delegates.all()), [user])
 
 
-class TestContributorCourseView(ViewTest):
+class TestContributorCourseView(WebTestWith200Check):
     url = '/contributor/course/%s' % TESTING_COURSE_ID
     test_users = ['editor', 'responsible']
 
@@ -107,13 +107,13 @@ def test_information_message(self):
         self.assertNotContains(page, "Please review the course's details below, add all contributors and select suitable questionnaires. Once everything is okay, please approve the course on the bottom of the page.")
 
 
-class TestContributorCoursePreviewView(ViewTest):
+class TestContributorCoursePreviewView(WebTestWith200Check):
     url = '/contributor/course/%s/preview' % TESTING_COURSE_ID
     test_users = ['editor', 'responsible']
 
     @classmethod
     def setUpTestData(cls):
-        cls.course = create_course_with_responsible_and_editor(course_id=TESTING_COURSE_ID)
+        create_course_with_responsible_and_editor(course_id=TESTING_COURSE_ID)
 
     def setUp(self):
         self.course = Course.objects.get(pk=TESTING_COURSE_ID)
@@ -124,13 +124,12 @@ def test_wrong_state(self):
         self.app.get(self.url, user='responsible', status=403)
 
 
-class TestContributorCourseEditView(ViewTest):
+class TestContributorCourseEditView(WebTest):
     url = '/contributor/course/%s/edit' % TESTING_COURSE_ID
-    test_users = ['editor', 'responsible']
 
     @classmethod
     def setUpTestData(cls):
-        cls.course = create_course_with_responsible_and_editor(course_id=TESTING_COURSE_ID)
+        create_course_with_responsible_and_editor(course_id=TESTING_COURSE_ID)
 
     def setUp(self):
         self.course = Course.objects.get(pk=TESTING_COURSE_ID)
diff --git a/evap/evaluation/tests/test_models.py b/evap/evaluation/tests/test_models.py
--- a/evap/evaluation/tests/test_models.py
+++ b/evap/evaluation/tests/test_models.py
@@ -5,11 +5,12 @@
 from django.core.cache import caches
 from django.core import mail
 
+from django_webtest import WebTest
 from model_mommy import mommy
 
 from evap.evaluation.models import (Contribution, Course, CourseType, EmailTemplate, NotArchiveable, Question,
                                     Questionnaire, RatingAnswerCounter, Semester, TextAnswer, UserProfile)
-from evap.evaluation.tests.tools import WebTest
+from evap.evaluation.tests.tools import let_user_vote_for_course
 from evap.results.tools import calculate_average_distribution
 from evap.results.views import get_course_result_template_fragment_cache_key
 
@@ -213,7 +214,7 @@ def test_adding_second_voter_sets_can_publish_text_results_to_true(self):
 
         self.assertFalse(course.can_publish_text_results)
 
-        self.let_user_vote_for_course(student2, course)
+        let_user_vote_for_course(self.app, student2, course)
         course = Course.objects.get(pk=course.pk)
 
         self.assertTrue(course.can_publish_text_results)
diff --git a/evap/evaluation/tests/test_views.py b/evap/evaluation/tests/test_views.py
--- a/evap/evaluation/tests/test_views.py
+++ b/evap/evaluation/tests/test_views.py
@@ -1,20 +1,20 @@
 from django.core import mail
 from django.contrib.auth.hashers import make_password
 
+from django_webtest import WebTest
 from model_mommy import mommy
 
 from evap.evaluation.models import UserProfile
-from evap.evaluation.tests.tools import ViewTest
+from evap.evaluation.tests.tools import WebTestWith200Check
 
 
-class TestIndexView(ViewTest):
+class TestIndexView(WebTest):
     url = '/'
-    test_users = ['']
 
     def test_passworduser_login(self):
         """ Tests whether a user can login with an incorrect and a correct password. """
         mommy.make(UserProfile, username='password.user', password=make_password('evap'))
-        response = self.app.get("/")
+        response = self.app.get(self.url)
         password_form = response.forms[0]
         password_form['username'] = 'password.user'
         password_form['password'] = 'asd'
@@ -27,7 +27,7 @@ def test_send_new_loginkey(self):
             shows the expected success message and sends only one email to the requesting
             user without people in cc even if the user has delegates and cc users. """
         mommy.make(UserProfile, email='asdf@example.com')
-        response = self.app.get("/")
+        response = self.app.get(self.url)
         email_form = response.forms[1]
         email_form['email'] = "doesnotexist@example.com"
         self.assertIn("No user with this email address was found", email_form.submit())
@@ -39,17 +39,17 @@ def test_send_new_loginkey(self):
         self.assertEqual(len(mail.outbox[0].cc), 0)
 
 
-class TestLegalNoticeView(ViewTest):
+class TestLegalNoticeView(WebTestWith200Check):
     url = '/legal_notice'
     test_users = ['']
 
 
-class TestFAQView(ViewTest):
+class TestFAQView(WebTestWith200Check):
     url = '/faq'
     test_users = ['']
 
 
-class TestContactEmail(ViewTest):
+class TestContactEmail(WebTest):
     csrf_checks = False
 
     def test_sends_mail(self):
@@ -58,7 +58,7 @@ def test_sends_mail(self):
         self.assertEqual(len(mail.outbox), 1)
 
 
-class TestChangeLanguageView(ViewTest):
+class TestChangeLanguageView(WebTest):
     url = '/set_lang'
     csrf_checks = False
 
diff --git a/evap/evaluation/tests/tools.py b/evap/evaluation/tests/tools.py
--- a/evap/evaluation/tests/tools.py
+++ b/evap/evaluation/tests/tools.py
@@ -1,6 +1,6 @@
 from django.http.request import QueryDict
 
-from django_webtest import WebTest as DjangoWebTest
+from django_webtest import WebTest
 from model_mommy import mommy
 
 from evap.evaluation.models import Contribution, Course, UserProfile, Questionnaire, Degree
@@ -29,22 +29,21 @@ def __repr__(self):
         return "[%d..%d]" % (self.lowest, self.highest)
 
 
-class WebTest(DjangoWebTest):
-    def let_user_vote_for_course(self, user, course):
-        url = '/student/vote/{}'.format(course.id)
-        page = self.app.get(url, user=user, status=200)
-        form = page.forms["student-vote-form"]
-        for contribution in course.contributions.all().prefetch_related("questionnaires", "questionnaires__questions"):
-            for questionnaire in contribution.questionnaires.all():
-                for question in questionnaire.questions.all():
-                    if question.type == "T":
-                        form[question_id(contribution, questionnaire, question)] = "Lorem ispum"
-                    elif question.type in ["L", "G", "P", "N"]:
-                        form[question_id(contribution, questionnaire, question)] = 1
-        form.submit()
+def let_user_vote_for_course(app, user, course):
+    url = '/student/vote/{}'.format(course.id)
+    page = app.get(url, user=user, status=200)
+    form = page.forms["student-vote-form"]
+    for contribution in course.contributions.all().prefetch_related("questionnaires", "questionnaires__questions"):
+        for questionnaire in contribution.questionnaires.all():
+            for question in questionnaire.questions.all():
+                if question.type == "T":
+                    form[question_id(contribution, questionnaire, question)] = "Lorem ispum"
+                elif question.type in ["L", "G", "P", "N"]:
+                    form[question_id(contribution, questionnaire, question)] = 1
+    form.submit()
 
 
-class ViewTest(WebTest):
+class WebTestWith200Check(WebTest):
     url = "/"
     test_users = []
 
diff --git a/evap/grades/tests.py b/evap/grades/tests.py
--- a/evap/grades/tests.py
+++ b/evap/grades/tests.py
@@ -3,13 +3,13 @@
 from django.core import mail
 from django.contrib.auth.models import Group
 
+from django_webtest import WebTest
 from model_mommy import mommy
 
 from evap.evaluation.models import UserProfile, Course, Questionnaire, Contribution, Semester
-from evap.evaluation.tests.tools import ViewTest, WebTest
 
 
-class GradeUploadTests(WebTest):
+class GradeUploadTest(WebTest):
     csrf_checks = False
 
     @classmethod
@@ -167,9 +167,8 @@ def test_grade_document_download_after_archiving(self):
         self.app.get(url, user="student", status=404)  # grades should not be downloadable anymore
 
 
-class GradeDocumentIndexTest(ViewTest):
+class GradeDocumentIndexTest(WebTest):
     url = '/grades/'
-    test_users = ['grade_publisher']
 
     @classmethod
     def setUpTestData(cls):
@@ -183,34 +182,37 @@ def test_visible_semesters(self):
         self.assertNotIn(self.archived_semester.name, page)
 
 
-class GradeDocumentSemesterViewTest(ViewTest):
+class GradeSemesterViewTest(WebTest):
     url = '/grades/semester/1'
-    test_users = ['grade_publisher']
 
     @classmethod
     def setUpTestData(cls):
         mommy.make(UserProfile, username="grade_publisher", groups=[Group.objects.get(name="Grade publisher")])
-        semester = mommy.make(Semester, pk=1, grade_documents_are_deleted=False)
-        mommy.make(Semester, pk=2, grade_documents_are_deleted=True)
-        cls.semester_course = mommy.make(Course, semester=semester, state="prepared")
 
-    def test_semester_pages(self):
+    def test_does_not_crash(self):
+        semester = mommy.make(Semester, pk=1, grade_documents_are_deleted=False)
+        semester_course = mommy.make(Course, semester=semester, state="prepared")
         page = self.app.get(self.url, user="grade_publisher", status=200)
-        self.assertIn(self.semester_course.name, page)
-        self.app.get('/grades/semester/2', user="grade_publisher", status=403)
+        self.assertIn(semester_course.name, page)
 
+    def test_403_on_deleted(self):
+        mommy.make(Semester, pk=1, grade_documents_are_deleted=True)
+        self.app.get('/grades/semester/1', user="grade_publisher", status=403)
 
-class GradeDocumentCourseViewTest(ViewTest):
+
+class GradeCourseViewTest(WebTest):
     url = '/grades/semester/1/course/1'
-    test_users = ['grade_publisher']
 
     @classmethod
     def setUpTestData(cls):
         mommy.make(UserProfile, username="grade_publisher", groups=[Group.objects.get(name="Grade publisher")])
+
+    def test_does_not_crash(self):
         semester = mommy.make(Semester, pk=1, grade_documents_are_deleted=False)
-        archived_semester = mommy.make(Semester, pk=2, grade_documents_are_deleted=True)
         mommy.make(Course, pk=1, semester=semester, state="prepared")
-        mommy.make(Course, pk=2, semester=archived_semester, state="prepared")
+        self.app.get('/grades/semester/1/course/1', user="grade_publisher", status=200)
 
-    def test_course_page(self):
-        self.app.get('/grades/semester/2/course/2', user="grade_publisher", status=403)
+    def test_403_on_archived_semester(self):
+        archived_semester = mommy.make(Semester, pk=1, grade_documents_are_deleted=True)
+        mommy.make(Course, pk=1, semester=archived_semester, state="prepared")
+        self.app.get('/grades/semester/1/course/1', user="grade_publisher", status=403)
diff --git a/evap/results/tests/test_views.py b/evap/results/tests/test_views.py
--- a/evap/results/tests/test_views.py
+++ b/evap/results/tests/test_views.py
@@ -2,17 +2,19 @@
 
 from django.contrib.auth.models import Group
 from django.test.testcases import TestCase
+
+from django_webtest import WebTest
 from model_mommy import mommy
 
 from evap.evaluation.models import Contribution, Course, Degree, Question, Questionnaire, RatingAnswerCounter, \
     Semester, UserProfile
-from evap.evaluation.tests.tools import ViewTest, WebTest
+from evap.evaluation.tests.tools import WebTestWith200Check, let_user_vote_for_course
 from evap.results.views import get_courses_with_prefetched_data
 
 import random
 
 
-class TestResultsView(ViewTest):
+class TestResultsView(WebTestWith200Check):
     url = '/results/'
     test_users = ['manager']
 
@@ -72,7 +74,7 @@ def test_few_answers_course_show_warning(self):
         self.assertIn("Only a few participants answered these questions.", page)
 
 
-class TestResultsSemesterCourseDetailView(ViewTest):
+class TestResultsSemesterCourseDetailView(WebTestWith200Check):
     url = '/results/semester/2/course/21'
     test_users = ['manager', 'contributor', 'responsible']
 
@@ -199,7 +201,7 @@ def helper_test_answer_visibility_two_voters(self, username):
         self.assertEqual(number_of_disabled_grade_badges, 1)
 
     def test_answer_visibility_one_voter(self):
-        self.let_user_vote_for_course(self.student1, self.course)
+        let_user_vote_for_course(self.app, self.student1, self.course)
         self.course.evaluation_end()
         self.course.review_finished()
         self.course.publish()
@@ -211,8 +213,8 @@ def test_answer_visibility_one_voter(self):
         self.helper_test_answer_visibility_one_voter("student", expect_page_not_visible=True)
 
     def test_answer_visibility_two_voters(self):
-        self.let_user_vote_for_course(self.student1, self.course)
-        self.let_user_vote_for_course(self.student2, self.course)
+        let_user_vote_for_course(self.app, self.student1, self.course)
+        let_user_vote_for_course(self.app, self.student2, self.course)
         self.course.evaluation_end()
         self.course.review_finished()
         self.course.publish()
diff --git a/evap/rewards/tests/test_views.py b/evap/rewards/tests/test_views.py
--- a/evap/rewards/tests/test_views.py
+++ b/evap/rewards/tests/test_views.py
@@ -3,15 +3,16 @@
 from django.contrib.auth.models import Group
 from django.urls import reverse
 
+from django_webtest import WebTest
 from model_mommy import mommy
 
 from evap.evaluation.models import UserProfile, Course, Semester
-from evap.evaluation.tests.tools import ViewTest
+from evap.evaluation.tests.tools import WebTestWith200Check
 from evap.rewards.models import RewardPointRedemptionEvent, RewardPointGranting, RewardPointRedemption, SemesterActivation
 from evap.rewards.tools import reward_points_of_user, is_semester_activated
 
 
-class TestEventDeleteView(ViewTest):
+class TestEventDeleteView(WebTest):
     url = reverse('rewards:reward_point_redemption_event_delete')
     csrf_checks = False
 
@@ -35,9 +36,8 @@ def test_deletion_failure(self):
         self.assertTrue(RewardPointRedemptionEvent.objects.filter(pk=event.pk).exists())
 
 
-class TestIndexView(ViewTest):
+class TestIndexView(WebTest):
     url = reverse('rewards:index')
-    test_users = ['student']
     csrf_checks = False
 
     @classmethod
@@ -49,7 +49,7 @@ def setUpTestData(cls):
         mommy.make(RewardPointRedemptionEvent, pk=2, redeem_end_date=date.today() + timedelta(days=1))
 
     def test_redeem_all_points(self):
-        response = self.app.get(reverse('rewards:index'), user='student')
+        response = self.app.get(self.url, user='student')
         form = response.forms['reward-redemption-form']
         form.set('points-1', 2)
         form.set('points-2', 3)
@@ -59,7 +59,7 @@ def test_redeem_all_points(self):
         self.assertEqual(0, reward_points_of_user(self.student))
 
     def test_redeem_too_many_points(self):
-        response = self.app.get(reverse('rewards:index'), user='student')
+        response = self.app.get(self.url, user='student')
         form = response.forms['reward-redemption-form']
         form.set('points-1', 3)
         form.set('points-2', 3)
@@ -69,7 +69,7 @@ def test_redeem_too_many_points(self):
 
     def test_redeem_points_for_expired_event(self):
         """ Regression test for #846 """
-        response = self.app.get(reverse('rewards:index'), user='student')
+        response = self.app.get(self.url, user='student')
         form = response.forms['reward-redemption-form']
         form.set('points-2', 1)
         RewardPointRedemptionEvent.objects.update(redeem_end_date=date.today() - timedelta(days=1))
@@ -78,7 +78,7 @@ def test_redeem_points_for_expired_event(self):
         self.assertEqual(5, reward_points_of_user(self.student))
 
 
-class TestEventsView(ViewTest):
+class TestEventsView(WebTestWith200Check):
     url = reverse('rewards:reward_point_redemption_events')
     test_users = ['manager']
 
@@ -89,9 +89,8 @@ def setUpTestData(cls):
         mommy.make(RewardPointRedemptionEvent, redeem_end_date=date.today() + timedelta(days=1))
 
 
-class TestEventCreateView(ViewTest):
+class TestEventCreateView(WebTest):
     url = reverse('rewards:reward_point_redemption_event_create')
-    test_users = ['manager']
     csrf_checks = False
 
     @classmethod
@@ -113,9 +112,8 @@ def test_create_redemption_event(self):
         self.assertEqual(RewardPointRedemptionEvent.objects.count(), 1)
 
 
-class TestEventEditView(ViewTest):
+class TestEventEditView(WebTest):
     url = reverse('rewards:reward_point_redemption_event_edit', args=[1])
-    test_users = ['manager']
     csrf_checks = False
 
     @classmethod
@@ -135,7 +133,7 @@ def test_edit_redemption_event(self):
         self.assertEqual(RewardPointRedemptionEvent.objects.get(pk=self.event.pk).name, 'new name')
 
 
-class TestExportView(ViewTest):
+class TestExportView(WebTestWith200Check):
     url = '/rewards/reward_point_redemption_event/1/export'
     test_users = ['manager']
 
@@ -146,7 +144,7 @@ def setUpTestData(cls):
         mommy.make(RewardPointRedemption, value=1, event=event)
 
 
-class TestSemesterActivationView(ViewTest):
+class TestSemesterActivationView(WebTest):
     url = '/rewards/reward_semester_activation/1/'
     csrf_checks = False
 
diff --git a/evap/staff/tests/test_views.py b/evap/staff/tests/test_views.py
--- a/evap/staff/tests/test_views.py
+++ b/evap/staff/tests/test_views.py
@@ -8,13 +8,15 @@
 from django.urls import reverse
 from django.test import override_settings
 from django.test.testcases import TestCase
+
+from django_webtest import WebTest
 from model_mommy import mommy
 import xlrd
 
 from evap.evaluation.models import Semester, UserProfile, Course, CourseType, TextAnswer, Contribution, \
                                    Questionnaire, Question, EmailTemplate, Degree, FaqSection, FaqQuestion, \
                                    RatingAnswerCounter
-from evap.evaluation.tests.tools import FuzzyInt, WebTest, ViewTest
+from evap.evaluation.tests.tools import FuzzyInt, let_user_vote_for_course, WebTestWith200Check
 from evap.rewards.models import SemesterActivation, RewardPointGranting
 from evap.staff.tools import generate_import_filename
 from evap.staff.views import get_courses_with_prefetched_data
@@ -27,8 +29,7 @@ def helper_delete_all_import_files(user_id):
 
 
 # Staff - Sample Files View
-class TestDownloadSampleXlsView(ViewTest):
-    test_users = ['manager']
+class TestDownloadSampleXlsView(WebTest):
     url = '/staff/download_sample_xls/sample.xls'
     email_placeholder = "institution.com"
 
@@ -53,7 +54,7 @@ def test_sample_file_correctness(self):
 
 
 # Staff - Root View
-class TestStaffIndexView(ViewTest):
+class TestStaffIndexView(WebTestWith200Check):
     test_users = ['manager']
     url = '/staff/'
 
@@ -63,7 +64,7 @@ def setUpTestData(cls):
 
 
 # Staff - FAQ View
-class TestStaffFAQView(ViewTest):
+class TestStaffFAQView(WebTestWith200Check):
     url = '/staff/faq/'
     test_users = ['manager']
 
@@ -72,7 +73,7 @@ def setUpTestData(cls):
         mommy.make(UserProfile, username='manager', groups=[Group.objects.get(name='Manager')])
 
 
-class TestStaffFAQEditView(ViewTest):
+class TestStaffFAQEditView(WebTestWith200Check):
     url = '/staff/faq/1'
     test_users = ['manager']
 
@@ -84,9 +85,8 @@ def setUpTestData(cls):
 
 
 # Staff - User Views
-class TestUserIndexView(ViewTest):
+class TestUserIndexView(WebTest):
     url = '/staff/user/'
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -106,9 +106,8 @@ def test_num_queries_is_constant(self):
             self.app.get(self.url, user="manager")
 
 
-class TestUserCreateView(ViewTest):
+class TestUserCreateView(WebTest):
     url = "/staff/user/create"
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -132,9 +131,8 @@ def test_user_is_created(self):
     (2 / 3, 2),
     (3 / 3, 3),
 ])
-class TestUserEditView(ViewTest):
+class TestUserEditView(WebTest):
     url = "/staff/user/3/edit"
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -167,7 +165,7 @@ def test_reward_points_granting_message(self):
         self.assertIn("The removal of courses has granted the user &quot;{}&quot; 3 reward points for the active semester.".format(student.username), page)
 
 
-class TestUserMergeSelectionView(ViewTest):
+class TestUserMergeSelectionView(WebTestWith200Check):
     url = "/staff/user/merge"
     test_users = ['manager']
 
@@ -177,7 +175,7 @@ def setUpTestData(cls):
         mommy.make(UserProfile)
 
 
-class TestUserMergeView(ViewTest):
+class TestUserMergeView(WebTestWith200Check):
     url = "/staff/user/3/merge/4"
     test_users = ['manager']
 
@@ -188,9 +186,8 @@ def setUpTestData(cls):
         mommy.make(UserProfile, pk=4)
 
 
-class TestUserBulkDeleteView(ViewTest):
+class TestUserBulkDeleteView(WebTest):
     url = '/staff/user/bulk_delete'
-    test_users = ['manager']
     filename = os.path.join(settings.BASE_DIR, 'staff/fixtures/test_user_bulk_delete_file.txt')
 
     @classmethod
@@ -249,9 +246,8 @@ def test_deletes_users(self):
         self.assertEqual(UserProfile.objects.exclude_inactive_users().count(), user_count_before - 2)
 
 
-class TestUserImportView(ViewTest):
+class TestUserImportView(WebTest):
     url = "/staff/user/import"
-    test_users = ["manager"]
     filename_valid = os.path.join(settings.BASE_DIR, "staff/fixtures/valid_user_import.xls")
     filename_invalid = os.path.join(settings.BASE_DIR, "staff/fixtures/invalid_user_import.xls")
     filename_random = os.path.join(settings.BASE_DIR, "staff/fixtures/random.random")
@@ -345,9 +341,8 @@ def test_invalid_import_operation(self):
 
 
 # Staff - Semester Views
-class TestSemesterView(ViewTest):
+class TestSemesterView(WebTest):
     url = '/staff/semester/1'
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -384,9 +379,8 @@ def test_get_courses_with_prefetched_data(self):
         get_courses_with_prefetched_data(course.semester)
 
 
-class TestSemesterCreateView(ViewTest):
+class TestSemesterCreateView(WebTest):
     url = '/staff/semester/create'
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -409,9 +403,8 @@ def test_create(self):
         self.assertEqual(Semester.objects.filter(name_de=name_de, name_en=name_en, short_name_de=short_name_de, short_name_en=short_name_en).count(), 1)
 
 
-class TestSemesterEditView(ViewTest):
+class TestSemesterEditView(WebTest):
     url = '/staff/semester/1/edit'
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -435,7 +428,7 @@ def test_name_change(self):
         self.assertEqual(self.semester.name_en, new_name_en)
 
 
-class TestSemesterDeleteView(ViewTest):
+class TestSemesterDeleteView(WebTest):
     url = '/staff/semester/delete'
     csrf_checks = False
 
@@ -459,9 +452,8 @@ def test_success(self):
         self.assertFalse(Semester.objects.filter(pk=semester.pk).exists())
 
 
-class TestSemesterAssignView(ViewTest):
+class TestSemesterAssignView(WebTest):
     url = '/staff/semester/1/assign'
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -489,7 +481,7 @@ def test_assign_questionnaires(self):
             self.assertEqual(course.general_contribution.questionnaires.get(), self.questionnaire)
 
 
-class TestSemesterTodoView(ViewTest):
+class TestSemesterTodoView(WebTestWith200Check):
     url = '/staff/semester/1/todo'
     test_users = ['manager']
 
@@ -508,9 +500,8 @@ def test_todo(self):
         self.assertContains(response, 'name_to_find')
 
 
-class TestSendReminderView(ViewTest):
+class TestSendReminderView(WebTest):
     url = '/staff/semester/1/responsible/3/send_reminder'
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -531,9 +522,8 @@ def test_form(self):
         self.assertIn("uiae", mail.outbox[0].body)
 
 
-class TestSemesterImportView(ViewTest):
+class TestSemesterImportView(WebTest):
     url = "/staff/semester/1/import"
-    test_users = ["manager"]
     filename_valid = os.path.join(settings.BASE_DIR, "staff/fixtures/test_enrollment_data.xls")
     filename_invalid = os.path.join(settings.BASE_DIR, "staff/fixtures/invalid_enrollment_data.xls")
     filename_random = os.path.join(settings.BASE_DIR, "staff/fixtures/random.random")
@@ -660,9 +650,8 @@ def test_missing_evaluation_period(self):
         self.assertContains(page, 'Import previously uploaded file')
 
 
-class TestSemesterExportView(ViewTest):
+class TestSemesterExportView(WebTest):
     url = '/staff/semester/1/export'
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -686,7 +675,7 @@ def test_view_downloads_excel_file(self):
                          'Evaluation {0}\n\n{1}'.format(self.semester.name, ", ".join([self.course_type.name])))
 
 
-class TestSemesterRawDataExportView(ViewTest):
+class TestSemesterRawDataExportView(WebTestWith200Check):
     url = '/staff/semester/1/raw_export'
     test_users = ['manager']
 
@@ -698,9 +687,9 @@ def setUpTestData(cls):
 
     def test_view_downloads_csv_file(self):
         student_user = mommy.make(UserProfile, username='student')
-        course1 = mommy.make(Course, type=self.course_type, semester=self.semester, participants=[student_user],
+        mommy.make(Course, type=self.course_type, semester=self.semester, participants=[student_user],
             voters=[student_user], name_de="1", name_en="Course 1")
-        course2 = mommy.make(Course, type=self.course_type, semester=self.semester, participants=[student_user],
+        mommy.make(Course, type=self.course_type, semester=self.semester, participants=[student_user],
             name_de="2", name_en="Course 2")
 
         response = self.app.get(self.url, user='manager')
@@ -723,9 +712,8 @@ def test_single_result(self):
         self.assertEqual(response.content, expected_content.encode("utf-8"))
 
 
-class TestSemesterParticipationDataExportView(ViewTest):
+class TestSemesterParticipationDataExportView(WebTest):
     url = '/staff/semester/1/participation_export'
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -753,7 +741,7 @@ def test_view_downloads_csv_file(self):
         self.assertEqual(response.content, expected_content.encode("utf-8"))
 
 
-class TestCourseOperationView(ViewTest):
+class TestCourseOperationView(WebTest):
     url = '/staff/semester/1/courseoperation'
 
     @classmethod
@@ -827,9 +815,8 @@ def test_operation_prepare(self):
         self.assertEqual(course.state, 'prepared')
 
 
-class TestSingleResultCreateView(ViewTest):
+class TestSingleResultCreateView(WebTest):
     url = '/staff/semester/1/singleresult/create'
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -862,9 +849,8 @@ def test_single_result_create(self):
 
 
 # Staff - Semester - Course Views
-class TestCourseCreateView(ViewTest):
+class TestCourseCreateView(WebTest):
     url = '/staff/semester/1/course/create'
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -878,7 +864,7 @@ def test_course_create(self):
         """
             Tests the course creation view with one valid and one invalid input dataset.
         """
-        response = self.app.get("/staff/semester/1/course/create", user="manager", status=200)
+        response = self.app.get(self.url, user="manager", status=200)
         form = response.forms["course-form"]
         form["name_de"] = "lfo9e7bmxp1xi"
         form["name_en"] = "asdf"
@@ -913,9 +899,8 @@ def test_course_create(self):
     (2 / 3, 2),
     (3 / 3, 3),
 ])
-class TestCourseEditView(ViewTest):
+class TestCourseEditView(WebTest):
     url = '/staff/semester/1/course/1/edit'
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -1054,7 +1039,7 @@ def test_last_modified_user(self):
         self.assertEqual(self.course.vote_end_date, old_vote_end_date)
 
 
-class TestSingleResultEditView(ViewTest):
+class TestSingleResultEditView(WebTestWith200Check):
     url = '/staff/semester/1/course/1/edit'
     test_users = ['manager']
 
@@ -1076,7 +1061,7 @@ def setUpTestData(cls):
         mommy.make(RatingAnswerCounter, question=question, contribution=contribution, answer=5, count=30)
 
 
-class TestCoursePreviewView(ViewTest):
+class TestCoursePreviewView(WebTestWith200Check):
     url = '/staff/semester/1/course/1/preview'
     test_users = ['manager']
 
@@ -1088,9 +1073,8 @@ def setUpTestData(cls):
         course.general_contribution.questionnaires.set([mommy.make(Questionnaire)])
 
 
-class TestCourseImportPersonsView(ViewTest):
+class TestCourseImportPersonsView(WebTest):
     url = "/staff/semester/1/course/1/person_import"
-    test_users = ["manager"]
     filename_valid = os.path.join(settings.BASE_DIR, "staff/fixtures/valid_user_import.xls")
     filename_invalid = os.path.join(settings.BASE_DIR, "staff/fixtures/invalid_user_import.xls")
     filename_random = os.path.join(settings.BASE_DIR, "staff/fixtures/random.random")
@@ -1279,9 +1263,8 @@ def test_invalid_participant_import_operation(self):
         self.assertEqual(reply.status_code, 400)
 
 
-class TestCourseEmailView(ViewTest):
+class TestCourseEmailView(WebTest):
     url = '/staff/semester/1/course/1/email'
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -1302,7 +1285,7 @@ def test_emails_are_sent(self):
         self.assertEqual(len(mail.outbox), 2)
 
 
-class TestCourseCommentView(ViewTest):
+class TestCourseCommentView(WebTest):
     url = '/staff/semester/1/course/1/comments'
 
     @classmethod
@@ -1326,23 +1309,23 @@ def test_comments_showing_up(self):
         self.app.get(self.url, user='manager', status=403)
 
         # add additional voter
-        self.let_user_vote_for_course(self.student2, self.course)
+        let_user_vote_for_course(self.app, self.student2, self.course)
 
         # now it should work
         self.app.get(self.url, user='manager', status=200)
 
     def test_comments_quick_view(self):
-        self.let_user_vote_for_course(self.student2, self.course)
+        let_user_vote_for_course(self.app, self.student2, self.course)
         page = self.app.get(self.url, user='manager', status=200)
         self.assertContains(page, self.answer)
 
     def test_comments_full_view(self):
-        self.let_user_vote_for_course(self.student2, self.course)
+        let_user_vote_for_course(self.app, self.student2, self.course)
         page = self.app.get(self.url + '?view=full', user='manager', status=200)
         self.assertContains(page, self.answer)
 
 
-class TestCourseCommentEditView(ViewTest):
+class TestCourseCommentEditView(WebTest):
     url = '/staff/semester/1/course/1/comment/00000000-0000-0000-0000-000000000001/edit'
 
     @classmethod
@@ -1365,7 +1348,7 @@ def test_comments_showing_up(self):
         self.app.get(self.url, user='manager', status=403)
 
         # add additional voter
-        self.let_user_vote_for_course(self.student2, self.course)
+        let_user_vote_for_course(self.app, self.student2, self.course)
 
         # now it should work
         response = self.app.get(self.url, user='manager')
@@ -1380,9 +1363,8 @@ def test_comments_showing_up(self):
 
 
 # Staff Questionnaire Views
-class TestQuestionnaireNewVersionView(ViewTest):
+class TestQuestionnaireNewVersionView(WebTest):
     url = '/staff/questionnaire/2/new_version'
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -1420,9 +1402,8 @@ def test_no_second_update(self):
         self.assertEqual(page.location, '/staff/questionnaire/')
 
 
-class TestQuestionnaireCreateView(ViewTest):
+class TestQuestionnaireCreateView(WebTest):
     url = "/staff/questionnaire/create"
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -1463,9 +1444,8 @@ def test_create_empty_questionnaire(self):
         self.assertFalse(Questionnaire.objects.filter(name_de="Test Fragebogen", name_en="test questionnaire").exists())
 
 
-class TestQuestionnaireIndexView(ViewTest):
+class TestQuestionnaireIndexView(WebTest):
     url = "/staff/questionnaire/"
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -1483,7 +1463,7 @@ def test_ordering(self):
         self.assertTrue(top_index < contributor_index < bottom_index)
 
 
-class TestQuestionnaireEditView(ViewTest):
+class TestQuestionnaireEditView(WebTestWith200Check):
     url = '/staff/questionnaire/2/edit'
     test_users = ['manager']
 
@@ -1522,7 +1502,7 @@ def test_allowed_type_changes_on_used_questionnaire(self):
         self.assertEqual(form['type'].options, [('20', True, 'Contributor questionnaire')])
 
 
-class TestQuestionnaireViewView(ViewTest):
+class TestQuestionnaireViewView(WebTestWith200Check):
     url = '/staff/questionnaire/2'
     test_users = ['manager']
 
@@ -1535,9 +1515,8 @@ def setUpTestData(cls):
         mommy.make(UserProfile, username="manager", groups=[Group.objects.get(name="Manager")])
 
 
-class TestQuestionnaireCopyView(ViewTest):
+class TestQuestionnaireCopyView(WebTest):
     url = '/staff/questionnaire/2/copy'
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -1592,9 +1571,8 @@ def test_questionnaire_deletion(self):
 
 
 # Staff Course Types Views
-class TestCourseTypeView(ViewTest):
+class TestCourseTypeView(WebTest):
     url = "/staff/course_types/"
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -1620,9 +1598,8 @@ def test_course_type_form(self):
         self.assertTrue(CourseType.objects.filter(name_de="Test", name_en="Test").exists())
 
 
-class TestCourseTypeMergeSelectionView(ViewTest):
+class TestCourseTypeMergeSelectionView(WebTest):
     url = "/staff/course_types/merge"
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -1639,9 +1616,8 @@ def test_same_course_fails(self):
         self.assertIn("You must select two different course types", str(response))
 
 
-class TestCourseTypeMergeView(ViewTest):
+class TestCourseTypeMergeView(WebTest):
     url = "/staff/course_types/1/merge/2"
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -1692,7 +1668,7 @@ def test_review_actions(self):
         # in a course with only one voter reviewing should fail
         self.helper(TextAnswer.NOT_REVIEWED, TextAnswer.PUBLISHED, "publish", expect_errors=True)
 
-        self.let_user_vote_for_course(self.student2, self.course)
+        let_user_vote_for_course(self.app, self.student2, self.course)
 
         # now reviewing should work
         self.helper(TextAnswer.NOT_REVIEWED, TextAnswer.PUBLISHED, "publish")
@@ -1721,9 +1697,8 @@ def test_raise_403(self):
         self.app.get(semester_url + "courseoperation", user="manager", status=403)
 
 
-class TestTemplateEditView(ViewTest):
+class TestTemplateEditView(WebTest):
     url = "/staff/template/1"
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -1746,9 +1721,8 @@ def test_emailtemplate(self):
         self.assertEqual(EmailTemplate.objects.get(pk=1).body, "body: mflkd862xmnbo5")
 
 
-class TestDegreeView(ViewTest):
+class TestDegreeView(WebTest):
     url = "/staff/degrees/"
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
@@ -1769,9 +1743,8 @@ def test_degree_form(self):
         self.assertTrue(Degree.objects.filter(name_de="Test", name_en="Test").exists())
 
 
-class TestSemesterQuestionnaireAssignment(ViewTest):
+class TestSemesterQuestionnaireAssignment(WebTest):
     url = "/staff/semester/1/assign"
-    test_users = ['manager']
 
     @classmethod
     def setUpTestData(cls):
diff --git a/evap/student/tests/test_views.py b/evap/student/tests/test_views.py
--- a/evap/student/tests/test_views.py
+++ b/evap/student/tests/test_views.py
@@ -1,14 +1,16 @@
 from django.test.utils import override_settings
 from django.urls import reverse
+
+from django_webtest import WebTest
 from model_mommy import mommy
 
 from evap.evaluation.models import UserProfile, Course, Questionnaire, Question, Contribution, TextAnswer, RatingAnswerCounter
-from evap.evaluation.tests.tools import ViewTest
+from evap.evaluation.tests.tools import WebTestWith200Check
 from evap.student.tools import question_id
 from evap.student.views import SUCCESS_MAGIC_STRING
 
 
-class TestStudentIndexView(ViewTest):
+class TestStudentIndexView(WebTestWith200Check):
     test_users = ['student']
     url = '/student/'
 
@@ -19,7 +21,7 @@ def setUp(self):
 
 
 @override_settings(INSTITUTION_EMAIL_DOMAINS=["example.com"])
-class TestVoteView(ViewTest):
+class TestVoteView(WebTest):
     url = '/student/vote/1'
 
     @classmethod