Repository: netbox-community/netbox

Issue:
Add tests for all individual create/edit/delete object views
### Proposed Changes
Extend `tests/test_views.py` within each app to add tests for the create, edit, and delete views for all apps.

### Justification
There is currently no coverage for these views, and establishing tests for them will help mitigate many bugs.


Discussion:
housekeeping always +1

Resolving patch:
diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
--- a/netbox/dcim/forms.py
+++ b/netbox/dcim/forms.py
@@ -1549,6 +1549,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
     )
     manufacturer = forms.ModelChoiceField(
         queryset=Manufacturer.objects.all(),
+        required=False,
         widget=APISelect(
             api_url="/api/dcim/manufacturers/",
             filter_for={


Test patch:
diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py
--- a/netbox/circuits/tests/test_views.py
+++ b/netbox/circuits/tests/test_views.py
@@ -1,23 +1,15 @@
-import urllib.parse
-
-from django.test import Client, TestCase
-from django.urls import reverse
+import datetime
 
+from circuits.choices import *
 from circuits.models import Circuit, CircuitType, Provider
-from utilities.testing import create_test_user
+from utilities.testing import StandardTestCases
 
 
-class ProviderTestCase(TestCase):
+class ProviderTestCase(StandardTestCases.Views):
+    model = Provider
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'circuits.view_provider',
-                'circuits.add_provider',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         Provider.objects.bulk_create([
             Provider(name='Provider 1', slug='provider-1', asn=65001),
@@ -25,48 +17,35 @@ def setUp(self):
             Provider(name='Provider 3', slug='provider-3', asn=65003),
         ])
 
-    def test_provider_list(self):
-
-        url = reverse('circuits:provider_list')
-        params = {
-            "q": "test",
+        cls.form_data = {
+            'name': 'Provider X',
+            'slug': 'provider-x',
+            'asn': 65123,
+            'account': '1234',
+            'portal_url': 'http://example.com/portal',
+            'noc_contact': 'noc@example.com',
+            'admin_contact': 'admin@example.com',
+            'comments': 'Another provider',
+            'tags': 'Alpha,Bravo,Charlie',
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_provider(self):
-
-        provider = Provider.objects.first()
-        response = self.client.get(provider.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_provider_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "Provider 4,provider-4",
             "Provider 5,provider-5",
             "Provider 6,provider-6",
         )
 
-        response = self.client.post(reverse('circuits:provider_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Provider.objects.count(), 6)
 
+class CircuitTypeTestCase(StandardTestCases.Views):
+    model = CircuitType
 
-class CircuitTypeTestCase(TestCase):
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'circuits.view_circuittype',
-                'circuits.add_circuittype',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         CircuitType.objects.bulk_create([
             CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
@@ -74,39 +53,25 @@ def setUp(self):
             CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
         ])
 
-    def test_circuittype_list(self):
-
-        url = reverse('circuits:circuittype_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-
-    def test_circuittype_import(self):
+        cls.form_data = {
+            'name': 'Circuit Type X',
+            'slug': 'circuit-type-x',
+            'description': 'A new circuit type',
+        }
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "Circuit Type 4,circuit-type-4",
             "Circuit Type 5,circuit-type-5",
             "Circuit Type 6,circuit-type-6",
         )
 
-        response = self.client.post(reverse('circuits:circuittype_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(CircuitType.objects.count(), 6)
-
 
-class CircuitTestCase(TestCase):
+class CircuitTestCase(StandardTestCases.Views):
+    model = Circuit
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'circuits.view_circuit',
-                'circuits.add_circuit',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         provider = Provider(name='Provider 1', slug='provider-1', asn=65001)
         provider.save()
@@ -120,33 +85,22 @@ def setUp(self):
             Circuit(cid='Circuit 3', provider=provider, type=circuittype),
         ])
 
-    def test_circuit_list(self):
-
-        url = reverse('circuits:circuit_list')
-        params = {
-            "provider": Provider.objects.first().slug,
-            "type": CircuitType.objects.first().slug,
+        cls.form_data = {
+            'cid': 'Circuit X',
+            'provider': provider.pk,
+            'type': circuittype.pk,
+            'status': CircuitStatusChoices.STATUS_ACTIVE,
+            'tenant': None,
+            'install_date': datetime.date(2020, 1, 1),
+            'commit_rate': 1000,
+            'description': 'A new circuit',
+            'comments': 'Some comments',
+            'tags': 'Alpha,Bravo,Charlie',
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_circuit(self):
-
-        circuit = Circuit.objects.first()
-        response = self.client.get(circuit.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_circuit_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "cid,provider,type",
             "Circuit 4,Provider 1,Circuit Type 1",
             "Circuit 5,Provider 1,Circuit Type 1",
             "Circuit 6,Provider 1,Circuit Type 1",
         )
-
-        response = self.client.post(reverse('circuits:circuit_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Circuit.objects.count(), 6)
diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -1,64 +1,56 @@
 import urllib.parse
+from decimal import Decimal
 
+import pytz
 import yaml
-from django.test import Client, TestCase
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
 from django.urls import reverse
 
 from dcim.choices import *
 from dcim.constants import *
 from dcim.models import *
-from utilities.testing import create_test_user
+from utilities.testing import StandardTestCases, TestCase
 
 
-class RegionTestCase(TestCase):
+class RegionTestCase(StandardTestCases.Views):
+    model = Region
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_region',
-                'dcim.add_region',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
-
-        # Create three Regions
-        for i in range(1, 4):
-            Region(name='Region {}'.format(i), slug='region-{}'.format(i)).save()
-
-    def test_region_list(self):
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
 
-        url = reverse('dcim:region_list')
+    @classmethod
+    def setUpTestData(cls):
 
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
+        # Create three Regions
+        regions = (
+            Region(name='Region 1', slug='region-1'),
+            Region(name='Region 2', slug='region-2'),
+            Region(name='Region 3', slug='region-3'),
+        )
+        for region in regions:
+            region.save()
 
-    def test_region_import(self):
+        cls.form_data = {
+            'name': 'Region X',
+            'slug': 'region-x',
+            'parent': regions[2].pk,
+        }
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "Region 4,region-4",
             "Region 5,region-5",
             "Region 6,region-6",
         )
 
-        response = self.client.post(reverse('dcim:region_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Region.objects.count(), 6)
+class SiteTestCase(StandardTestCases.Views):
+    model = Site
 
-
-class SiteTestCase(TestCase):
-
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_site',
-                'dcim.add_site',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         region = Region(name='Region 1', slug='region-1')
         region.save()
@@ -69,48 +61,44 @@ def setUp(self):
             Site(name='Site 3', slug='site-3', region=region),
         ])
 
-    def test_site_list(self):
-
-        url = reverse('dcim:site_list')
-        params = {
-            "region": Region.objects.first().slug,
+        cls.form_data = {
+            'name': 'Site X',
+            'slug': 'site-x',
+            'status': SiteStatusChoices.STATUS_PLANNED,
+            'region': region.pk,
+            'tenant': None,
+            'facility': 'Facility X',
+            'asn': 65001,
+            'time_zone': pytz.UTC,
+            'description': 'Site description',
+            'physical_address': '742 Evergreen Terrace, Springfield, USA',
+            'shipping_address': '742 Evergreen Terrace, Springfield, USA',
+            'latitude': Decimal('35.780000'),
+            'longitude': Decimal('-78.642000'),
+            'contact_name': 'Hank Hill',
+            'contact_phone': '123-555-9999',
+            'contact_email': 'hank@stricklandpropane.com',
+            'comments': 'Test site',
+            'tags': 'Alpha,Bravo,Charlie',
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_site(self):
-
-        site = Site.objects.first()
-        response = self.client.get(site.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_site_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "Site 4,site-4",
             "Site 5,site-5",
             "Site 6,site-6",
         )
 
-        response = self.client.post(reverse('dcim:site_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Site.objects.count(), 6)
 
+class RackGroupTestCase(StandardTestCases.Views):
+    model = RackGroup
 
-class RackGroupTestCase(TestCase):
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_rackgroup',
-                'dcim.add_rackgroup',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -121,39 +109,29 @@ def setUp(self):
             RackGroup(name='Rack Group 3', slug='rack-group-3', site=site),
         ])
 
-    def test_rackgroup_list(self):
-
-        url = reverse('dcim:rackgroup_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-
-    def test_rackgroup_import(self):
+        cls.form_data = {
+            'name': 'Rack Group X',
+            'slug': 'rack-group-x',
+            'site': site.pk,
+        }
 
-        csv_data = (
+        cls.csv_data = (
             "site,name,slug",
             "Site 1,Rack Group 4,rack-group-4",
             "Site 1,Rack Group 5,rack-group-5",
             "Site 1,Rack Group 6,rack-group-6",
         )
 
-        response = self.client.post(reverse('dcim:rackgroup_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(RackGroup.objects.count(), 6)
 
+class RackRoleTestCase(StandardTestCases.Views):
+    model = RackRole
 
-class RackRoleTestCase(TestCase):
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_rackrole',
-                'dcim.add_rackrole',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         RackRole.objects.bulk_create([
             RackRole(name='Rack Role 1', slug='rack-role-1'),
@@ -161,34 +139,33 @@ def setUp(self):
             RackRole(name='Rack Role 3', slug='rack-role-3'),
         ])
 
-    def test_rackrole_list(self):
-
-        url = reverse('dcim:rackrole_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-
-    def test_rackrole_import(self):
+        cls.form_data = {
+            'name': 'Rack Role X',
+            'slug': 'rack-role-x',
+            'color': 'c0c0c0',
+            'description': 'New role',
+        }
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug,color",
             "Rack Role 4,rack-role-4,ff0000",
             "Rack Role 5,rack-role-5,00ff00",
             "Rack Role 6,rack-role-6,0000ff",
         )
 
-        response = self.client.post(reverse('dcim:rackrole_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(RackRole.objects.count(), 6)
+class RackReservationTestCase(StandardTestCases.Views):
+    model = RackReservation
 
+    # Disable inapplicable tests
+    test_get_object = None
+    test_create_object = None  # TODO: Fix URL name for view
+    test_import_objects = None
 
-class RackReservationTestCase(TestCase):
+    @classmethod
+    def setUpTestData(cls):
 
-    def setUp(self):
-        user = create_test_user(permissions=['dcim.view_rackreservation'])
-        self.client = Client()
-        self.client.force_login(user)
+        user = User.objects.create_user(username='testuser2')
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -202,28 +179,27 @@ def setUp(self):
             RackReservation(rack=rack, user=user, units=[7, 8, 9], description='Reservation 3'),
         ])
 
-    def test_rackreservation_list(self):
+        cls.form_data = {
+            'rack': rack.pk,
+            'units': [10, 11, 12],
+            'user': user.pk,
+            'tenant': None,
+            'description': 'New reservation',
+        }
 
-        url = reverse('dcim:rackreservation_list')
 
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
+class RackTestCase(StandardTestCases.Views):
+    model = Rack
 
+    # TODO: Remove this when #4067 is fixed
+    test_create_object = None
 
-class RackTestCase(TestCase):
+    @classmethod
+    def setUpTestData(cls):
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_rack',
-                'dcim.add_rack',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
-
-        site = Site(name='Site 1', slug='site-1')
-        site.save()
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        rackgroup = RackGroup.objects.create(name='Rack Group 1', slug='rack-group-1', site=site)
+        rackrole = RackRole.objects.create(name='Rack Role 1', slug='rack-role-1')
 
         Rack.objects.bulk_create([
             Rack(name='Rack 1', site=site),
@@ -231,48 +207,44 @@ def setUp(self):
             Rack(name='Rack 3', site=site),
         ])
 
-    def test_rack_list(self):
-
-        url = reverse('dcim:rack_list')
-        params = {
-            "site": Site.objects.first().slug,
+        cls.form_data = {
+            'name': 'Rack X',
+            'facility_id': 'Facility X',
+            'site': site.pk,
+            'group': rackgroup.pk,
+            'tenant': None,
+            'status': RackStatusChoices.STATUS_PLANNED,
+            'role': rackrole.pk,
+            'serial': '123456',
+            'asset_tag': 'ABCDEF',
+            'type': RackTypeChoices.TYPE_CABINET,
+            'width': RackWidthChoices.WIDTH_19IN,
+            'u_height': 48,
+            'desc_units': False,
+            'outer_width': 500,
+            'outer_depth': 500,
+            'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER,
+            'comments': 'Some comments',
+            'tags': 'Alpha,Bravo,Charlie',
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_rack(self):
-
-        rack = Rack.objects.first()
-        response = self.client.get(rack.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_rack_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "site,name,width,u_height",
             "Site 1,Rack 4,19,42",
             "Site 1,Rack 5,19,42",
             "Site 1,Rack 6,19,42",
         )
 
-        response = self.client.post(reverse('dcim:rack_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Rack.objects.count(), 6)
+class ManufacturerTestCase(StandardTestCases.Views):
+    model = Manufacturer
 
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
 
-class ManufacturerTypeTestCase(TestCase):
-
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_manufacturer',
-                'dcim.add_manufacturer',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         Manufacturer.objects.bulk_create([
             Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
@@ -280,34 +252,24 @@ def setUp(self):
             Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
         ])
 
-    def test_manufacturer_list(self):
-
-        url = reverse('dcim:manufacturer_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-
-    def test_manufacturer_import(self):
+        cls.form_data = {
+            'name': 'Manufacturer X',
+            'slug': 'manufacturer-x',
+        }
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "Manufacturer 4,manufacturer-4",
             "Manufacturer 5,manufacturer-5",
             "Manufacturer 6,manufacturer-6",
         )
 
-        response = self.client.post(reverse('dcim:manufacturer_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Manufacturer.objects.count(), 6)
 
+class DeviceTypeTestCase(StandardTestCases.Views):
+    model = DeviceType
 
-class DeviceTypeTestCase(TestCase):
-
-    def setUp(self):
-        user = create_test_user(permissions=['dcim.view_devicetype'])
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
         manufacturer.save()
@@ -318,35 +280,22 @@ def setUp(self):
             DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturer),
         ])
 
-    def test_devicetype_list(self):
-
-        url = reverse('dcim:devicetype_list')
-        params = {
-            "manufacturer": Manufacturer.objects.first().slug,
+        cls.form_data = {
+            'manufacturer': manufacturer.pk,
+            'model': 'Device Type X',
+            'slug': 'device-type-x',
+            'part_number': '123ABC',
+            'u_height': 2,
+            'is_full_depth': True,
+            'subdevice_role': '',  # CharField
+            'comments': 'Some comments',
+            'tags': 'Alpha,Bravo,Charlie',
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_devicetype_export(self):
-
-        url = reverse('dcim:devicetype_list')
-
-        response = self.client.get('{}?export'.format(url))
-        self.assertEqual(response.status_code, 200)
-        data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader))
-        self.assertEqual(len(data), 3)
-        self.assertEqual(data[0]['manufacturer'], 'Manufacturer 1')
-        self.assertEqual(data[0]['model'], 'Device Type 1')
-
-    def test_devicetype(self):
-
-        devicetype = DeviceType.objects.first()
-        response = self.client.get(devicetype.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_devicetype_import(self):
-
+    def test_import_objects(self):
+        """
+        Custom import test for YAML-based imports (versus CSV)
+        """
         IMPORT_DATA = """
 manufacturer: Generic
 model: TEST-1000
@@ -420,8 +369,8 @@ def test_devicetype_import(self):
         # Create the manufacturer
         Manufacturer(name='Generic', slug='generic').save()
 
-        # Authenticate as user with necessary permissions
-        user = create_test_user(username='testuser2', permissions=[
+        # Add all required permissions to the test user
+        self.add_permissions(
             'dcim.view_devicetype',
             'dcim.add_devicetype',
             'dcim.add_consoleporttemplate',
@@ -432,15 +381,14 @@ def test_devicetype_import(self):
             'dcim.add_frontporttemplate',
             'dcim.add_rearporttemplate',
             'dcim.add_devicebaytemplate',
-        ])
-        self.client.force_login(user)
+        )
 
         form_data = {
             'data': IMPORT_DATA,
             'format': 'yaml'
         }
         response = self.client.post(reverse('dcim:devicetype_import'), data=form_data, follow=True)
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
 
         dt = DeviceType.objects.get(model='TEST-1000')
 
@@ -487,18 +435,28 @@ def test_devicetype_import(self):
         db1 = DeviceBayTemplate.objects.first()
         self.assertEqual(db1.name, 'Device Bay 1')
 
+    def test_devicetype_export(self):
 
-class DeviceRoleTestCase(TestCase):
+        url = reverse('dcim:devicetype_list')
+        self.add_permissions('dcim.view_devicetype')
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_devicerole',
-                'dcim.add_devicerole',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+        response = self.client.get('{}?export'.format(url))
+        self.assertEqual(response.status_code, 200)
+        data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader))
+        self.assertEqual(len(data), 3)
+        self.assertEqual(data[0]['manufacturer'], 'Manufacturer 1')
+        self.assertEqual(data[0]['model'], 'Device Type 1')
+
+
+class DeviceRoleTestCase(StandardTestCases.Views):
+    model = DeviceRole
+
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
+
+    @classmethod
+    def setUpTestData(cls):
 
         DeviceRole.objects.bulk_create([
             DeviceRole(name='Device Role 1', slug='device-role-1'),
@@ -506,91 +464,68 @@ def setUp(self):
             DeviceRole(name='Device Role 3', slug='device-role-3'),
         ])
 
-    def test_devicerole_list(self):
-
-        url = reverse('dcim:devicerole_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-
-    def test_devicerole_import(self):
+        cls.form_data = {
+            'name': 'Devie Role X',
+            'slug': 'device-role-x',
+            'color': 'c0c0c0',
+            'vm_role': False,
+            'description': 'New device role',
+        }
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug,color",
             "Device Role 4,device-role-4,ff0000",
             "Device Role 5,device-role-5,00ff00",
             "Device Role 6,device-role-6,0000ff",
         )
 
-        response = self.client.post(reverse('dcim:devicerole_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(DeviceRole.objects.count(), 6)
+class PlatformTestCase(StandardTestCases.Views):
+    model = Platform
 
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
 
-class PlatformTestCase(TestCase):
+    @classmethod
+    def setUpTestData(cls):
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_platform',
-                'dcim.add_platform',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
 
         Platform.objects.bulk_create([
-            Platform(name='Platform 1', slug='platform-1'),
-            Platform(name='Platform 2', slug='platform-2'),
-            Platform(name='Platform 3', slug='platform-3'),
+            Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturer),
+            Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturer),
+            Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturer),
         ])
 
-    def test_platform_list(self):
-
-        url = reverse('dcim:platform_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-
-    def test_platform_import(self):
+        cls.form_data = {
+            'name': 'Platform X',
+            'slug': 'platform-x',
+            'manufacturer': manufacturer.pk,
+            'napalm_driver': 'junos',
+            'napalm_args': None,
+        }
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "Platform 4,platform-4",
             "Platform 5,platform-5",
             "Platform 6,platform-6",
         )
 
-        response = self.client.post(reverse('dcim:platform_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Platform.objects.count(), 6)
-
-
-class DeviceTestCase(TestCase):
-
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_device',
-                'dcim.add_device',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
-
-        site = Site(name='Site 1', slug='site-1')
-        site.save()
 
-        manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
-        manufacturer.save()
+class DeviceTestCase(StandardTestCases.Views):
+    model = Device
 
-        devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
-        devicetype.save()
+    @classmethod
+    def setUpTestData(cls):
 
-        devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
-        devicerole.save()
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        rack = Rack.objects.create(name='Rack 1', site=site)
+        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
+        devicetype = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
+        devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
+        platform = Platform.objects.create(name='Platform 1', slug='platform-1')
 
         Device.objects.bulk_create([
             Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole),
@@ -598,49 +533,46 @@ def setUp(self):
             Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole),
         ])
 
-    def test_device_list(self):
-
-        url = reverse('dcim:device_list')
-        params = {
-            "device_type_id": DeviceType.objects.first().pk,
-            "role": DeviceRole.objects.first().slug,
+        cls.form_data = {
+            'device_type': devicetype.pk,
+            'device_role': devicerole.pk,
+            'tenant': None,
+            'platform': platform.pk,
+            'name': 'Device X',
+            'serial': '123456',
+            'asset_tag': 'ABCDEF',
+            'site': site.pk,
+            'rack': rack.pk,
+            'position': 1,
+            'face': DeviceFaceChoices.FACE_FRONT,
+            'status': DeviceStatusChoices.STATUS_PLANNED,
+            'primary_ip4': None,
+            'primary_ip6': None,
+            'cluster': None,
+            'virtual_chassis': None,
+            'vc_position': None,
+            'vc_priority': None,
+            'comments': 'A new device',
+            'tags': 'Alpha,Bravo,Charlie',
+            'local_context_data': None,
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_device(self):
-
-        device = Device.objects.first()
-        response = self.client.get(device.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_device_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "device_role,manufacturer,model_name,status,site,name",
             "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 4",
             "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 5",
             "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 6",
         )
 
-        response = self.client.post(reverse('dcim:device_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Device.objects.count(), 6)
-
 
+# TODO: Convert to StandardTestCases.Views
 class ConsolePortTestCase(TestCase):
+    user_permissions = (
+        'dcim.view_consoleport',
+    )
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_consoleport',
-                'dcim.add_consoleport',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -668,9 +600,10 @@ def test_consoleport_list(self):
         url = reverse('dcim:consoleport_list')
 
         response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
 
     def test_consoleport_import(self):
+        self.add_permissions('dcim.add_consoleport')
 
         csv_data = (
             "device,name",
@@ -681,21 +614,18 @@ def test_consoleport_import(self):
 
         response = self.client.post(reverse('dcim:consoleport_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
         self.assertEqual(ConsolePort.objects.count(), 6)
 
 
+# TODO: Convert to StandardTestCases.Views
 class ConsoleServerPortTestCase(TestCase):
+    user_permissions = (
+        'dcim.view_consoleserverport',
+    )
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_consoleserverport',
-                'dcim.add_consoleserverport',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -723,9 +653,10 @@ def test_consoleserverport_list(self):
         url = reverse('dcim:consoleserverport_list')
 
         response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
 
     def test_consoleserverport_import(self):
+        self.add_permissions('dcim.add_consoleserverport')
 
         csv_data = (
             "device,name",
@@ -736,21 +667,18 @@ def test_consoleserverport_import(self):
 
         response = self.client.post(reverse('dcim:consoleserverport_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
         self.assertEqual(ConsoleServerPort.objects.count(), 6)
 
 
+# TODO: Convert to StandardTestCases.Views
 class PowerPortTestCase(TestCase):
+    user_permissions = (
+        'dcim.view_powerport',
+    )
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_powerport',
-                'dcim.add_powerport',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -778,9 +706,10 @@ def test_powerport_list(self):
         url = reverse('dcim:powerport_list')
 
         response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
 
     def test_powerport_import(self):
+        self.add_permissions('dcim.add_powerport')
 
         csv_data = (
             "device,name",
@@ -791,21 +720,18 @@ def test_powerport_import(self):
 
         response = self.client.post(reverse('dcim:powerport_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
         self.assertEqual(PowerPort.objects.count(), 6)
 
 
+# TODO: Convert to StandardTestCases.Views
 class PowerOutletTestCase(TestCase):
+    user_permissions = (
+        'dcim.view_poweroutlet',
+    )
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_poweroutlet',
-                'dcim.add_poweroutlet',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -833,9 +759,10 @@ def test_poweroutlet_list(self):
         url = reverse('dcim:poweroutlet_list')
 
         response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
 
     def test_poweroutlet_import(self):
+        self.add_permissions('dcim.add_poweroutlet')
 
         csv_data = (
             "device,name",
@@ -846,21 +773,18 @@ def test_poweroutlet_import(self):
 
         response = self.client.post(reverse('dcim:poweroutlet_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
         self.assertEqual(PowerOutlet.objects.count(), 6)
 
 
+# TODO: Convert to StandardTestCases.Views
 class InterfaceTestCase(TestCase):
+    user_permissions = (
+        'dcim.view_interface',
+    )
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_interface',
-                'dcim.add_interface',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -888,9 +812,10 @@ def test_interface_list(self):
         url = reverse('dcim:interface_list')
 
         response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
 
     def test_interface_import(self):
+        self.add_permissions('dcim.add_interface')
 
         csv_data = (
             "device,name,type",
@@ -901,21 +826,18 @@ def test_interface_import(self):
 
         response = self.client.post(reverse('dcim:interface_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
         self.assertEqual(Interface.objects.count(), 6)
 
 
+# TODO: Convert to StandardTestCases.Views
 class FrontPortTestCase(TestCase):
+    user_permissions = (
+        'dcim.view_frontport',
+    )
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_frontport',
-                'dcim.add_frontport',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -955,9 +877,10 @@ def test_frontport_list(self):
         url = reverse('dcim:frontport_list')
 
         response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
 
     def test_frontport_import(self):
+        self.add_permissions('dcim.add_frontport')
 
         csv_data = (
             "device,name,type,rear_port,rear_port_position",
@@ -968,21 +891,18 @@ def test_frontport_import(self):
 
         response = self.client.post(reverse('dcim:frontport_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
         self.assertEqual(FrontPort.objects.count(), 6)
 
 
+# TODO: Convert to StandardTestCases.Views
 class RearPortTestCase(TestCase):
+    user_permissions = (
+        'dcim.view_rearport',
+    )
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_rearport',
-                'dcim.add_rearport',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -1010,9 +930,10 @@ def test_rearport_list(self):
         url = reverse('dcim:rearport_list')
 
         response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
 
     def test_rearport_import(self):
+        self.add_permissions('dcim.add_rearport')
 
         csv_data = (
             "device,name,type,positions",
@@ -1023,21 +944,18 @@ def test_rearport_import(self):
 
         response = self.client.post(reverse('dcim:rearport_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
         self.assertEqual(RearPort.objects.count(), 6)
 
 
+# TODO: Convert to StandardTestCases.Views
 class DeviceBayTestCase(TestCase):
+    user_permissions = (
+        'dcim.view_devicebay',
+    )
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_devicebay',
-                'dcim.add_devicebay',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -1069,9 +987,10 @@ def test_devicebay_list(self):
         url = reverse('dcim:devicebay_list')
 
         response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
 
     def test_devicebay_import(self):
+        self.add_permissions('dcim.add_devicebay')
 
         csv_data = (
             "device,name",
@@ -1082,21 +1001,18 @@ def test_devicebay_import(self):
 
         response = self.client.post(reverse('dcim:devicebay_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
         self.assertEqual(DeviceBay.objects.count(), 6)
 
 
+# TODO: Convert to StandardTestCases.Views
 class InventoryItemTestCase(TestCase):
+    user_permissions = (
+        'dcim.view_inventoryitem',
+    )
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_inventoryitem',
-                'dcim.add_inventoryitem',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
@@ -1127,9 +1043,10 @@ def test_inventoryitem_list(self):
         }
 
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
 
     def test_inventoryitem_import(self):
+        self.add_permissions('dcim.add_inventoryitem')
 
         csv_data = (
             "device,name",
@@ -1140,34 +1057,23 @@ def test_inventoryitem_import(self):
 
         response = self.client.post(reverse('dcim:inventoryitem_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
         self.assertEqual(InventoryItem.objects.count(), 6)
 
 
-class CableTestCase(TestCase):
+class CableTestCase(StandardTestCases.Views):
+    model = Cable
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'dcim.view_cable',
-                'dcim.add_cable',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    # TODO: Creation URL needs termination context
+    test_create_object = None
 
-        site = Site(name='Site 1', slug='site-1')
-        site.save()
-
-        manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
-        manufacturer.save()
-
-        devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
-        devicetype.save()
-
-        devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
-        devicerole.save()
+    @classmethod
+    def setUpTestData(cls):
 
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
+        devicetype = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
+        devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
         device1 = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
         device1.save()
         device2 = Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole)
@@ -1177,68 +1083,62 @@ def setUp(self):
         device4 = Device(name='Device 4', site=site, device_type=devicetype, device_role=devicerole)
         device4.save()
 
-        iface1 = Interface(device=device1, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED)
-        iface1.save()
-        iface2 = Interface(device=device1, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED)
-        iface2.save()
-        iface3 = Interface(device=device1, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED)
-        iface3.save()
-        iface4 = Interface(device=device2, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED)
-        iface4.save()
-        iface5 = Interface(device=device2, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED)
-        iface5.save()
-        iface6 = Interface(device=device2, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED)
-        iface6.save()
-
-        # Interfaces for CSV import testing
-        Interface(device=device3, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save()
-        Interface(device=device3, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save()
-        Interface(device=device3, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save()
-        Interface(device=device4, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save()
-        Interface(device=device4, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save()
-        Interface(device=device4, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save()
-
-        Cable(termination_a=iface1, termination_b=iface4, type=CableTypeChoices.TYPE_CAT6).save()
-        Cable(termination_a=iface2, termination_b=iface5, type=CableTypeChoices.TYPE_CAT6).save()
-        Cable(termination_a=iface3, termination_b=iface6, type=CableTypeChoices.TYPE_CAT6).save()
-
-    def test_cable_list(self):
-
-        url = reverse('dcim:cable_list')
-        params = {
-            "type": CableTypeChoices.TYPE_CAT6,
+        interfaces = (
+            Interface(device=device1, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=device1, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=device1, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=device2, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=device2, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=device2, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=device3, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=device3, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=device3, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=device4, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=device4, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=device4, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+        )
+        Interface.objects.bulk_create(interfaces)
+
+        Cable(termination_a=interfaces[0], termination_b=interfaces[3], type=CableTypeChoices.TYPE_CAT6).save()
+        Cable(termination_a=interfaces[1], termination_b=interfaces[4], type=CableTypeChoices.TYPE_CAT6).save()
+        Cable(termination_a=interfaces[2], termination_b=interfaces[5], type=CableTypeChoices.TYPE_CAT6).save()
+
+        interface_ct = ContentType.objects.get_for_model(Interface)
+        cls.form_data = {
+            # Changing terminations not supported when editing an existing Cable
+            'termination_a_type': interface_ct.pk,
+            'termination_a_id': interfaces[0].pk,
+            'termination_b_type': interface_ct.pk,
+            'termination_b_id': interfaces[3].pk,
+            'type': CableTypeChoices.TYPE_CAT6,
+            'status': CableStatusChoices.STATUS_PLANNED,
+            'label': 'New cable',
+            'color': 'c0c0c0',
+            'length': 100,
+            'length_unit': CableLengthUnitChoices.UNIT_FOOT,
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_cable(self):
-
-        cable = Cable.objects.first()
-        response = self.client.get(cable.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_cable_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "side_a_device,side_a_type,side_a_name,side_b_device,side_b_type,side_b_name",
             "Device 3,interface,Interface 1,Device 4,interface,Interface 1",
             "Device 3,interface,Interface 2,Device 4,interface,Interface 2",
             "Device 3,interface,Interface 3,Device 4,interface,Interface 3",
         )
 
-        response = self.client.post(reverse('dcim:cable_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Cable.objects.count(), 6)
+class VirtualChassisTestCase(StandardTestCases.Views):
+    model = VirtualChassis
 
+    # Disable inapplicable tests
+    test_get_object = None
+    test_import_objects = None
 
-class VirtualChassisTestCase(TestCase):
+    # TODO: Requires special form handling
+    test_create_object = None
+    test_edit_object = None
 
-    def setUp(self):
-        user = create_test_user(permissions=['dcim.view_virtualchassis'])
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         site = Site.objects.create(name='Site 1', slug='site-1')
         manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1')
@@ -1276,10 +1176,3 @@ def setUp(self):
         Device.objects.filter(pk=device4.pk).update(virtual_chassis=vc2, vc_position=2)
         vc3 = VirtualChassis.objects.create(master=device5, domain='test-domain-3')
         Device.objects.filter(pk=device6.pk).update(virtual_chassis=vc3, vc_position=2)
-
-    def test_virtualchassis_list(self):
-
-        url = reverse('dcim:virtualchassis_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py
--- a/netbox/extras/tests/test_views.py
+++ b/netbox/extras/tests/test_views.py
@@ -2,48 +2,55 @@
 import uuid
 
 from django.contrib.auth.models import User
-from django.test import Client, TestCase
 from django.urls import reverse
 
 from dcim.models import Site
 from extras.choices import ObjectChangeActionChoices
 from extras.models import ConfigContext, ObjectChange, Tag
-from utilities.testing import create_test_user
+from utilities.testing import StandardTestCases, TestCase
 
 
-class TagTestCase(TestCase):
+class TagTestCase(StandardTestCases.Views):
+    model = Tag
 
-    def setUp(self):
-        user = create_test_user(permissions=['extras.view_tag'])
-        self.client = Client()
-        self.client.force_login(user)
+    # Disable inapplicable tests
+    test_create_object = None
+    test_import_objects = None
 
-        Tag.objects.bulk_create([
+    # TODO: Restore test when #4071 is resolved
+    test_get_object = None
+
+    @classmethod
+    def setUpTestData(cls):
+
+        Tag.objects.bulk_create((
             Tag(name='Tag 1', slug='tag-1'),
             Tag(name='Tag 2', slug='tag-2'),
             Tag(name='Tag 3', slug='tag-3'),
-        ])
+        ))
 
-    def test_tag_list(self):
-
-        url = reverse('extras:tag_list')
-        params = {
-            "q": "tag",
+        cls.form_data = {
+            'name': 'Tag X',
+            'slug': 'tag-x',
+            'color': 'c0c0c0',
+            'comments': 'Some comments',
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
 
+class ConfigContextTestCase(StandardTestCases.Views):
+    model = ConfigContext
 
-class ConfigContextTestCase(TestCase):
+    # Disable inapplicable tests
+    test_import_objects = None
 
-    def setUp(self):
-        user = create_test_user(permissions=['extras.view_configcontext'])
-        self.client = Client()
-        self.client.force_login(user)
+    # TODO: Resolve model discrepancies when creating/editing ConfigContexts
+    test_create_object = None
+    test_edit_object = None
 
-        site = Site(name='Site 1', slug='site-1')
-        site.save()
+    @classmethod
+    def setUpTestData(cls):
+
+        site = Site.objects.create(name='Site 1', slug='site-1')
 
         # Create three ConfigContexts
         for i in range(1, 4):
@@ -54,34 +61,35 @@ def setUp(self):
             configcontext.save()
             configcontext.sites.add(site)
 
-    def test_configcontext_list(self):
-
-        url = reverse('extras:configcontext_list')
-        params = {
-            "q": "foo",
+        cls.form_data = {
+            'name': 'Config Context X',
+            'weight': 200,
+            'description': 'A new config context',
+            'is_active': True,
+            'regions': [],
+            'sites': [site.pk],
+            'roles': [],
+            'platforms': [],
+            'tenant_groups': [],
+            'tenants': [],
+            'tags': [],
+            'data': '{"foo": 123}',
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_configcontext(self):
-
-        configcontext = ConfigContext.objects.first()
-        response = self.client.get(configcontext.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
 
 class ObjectChangeTestCase(TestCase):
+    user_permissions = (
+        'extras.view_objectchange',
+    )
 
-    def setUp(self):
-        user = create_test_user(permissions=['extras.view_objectchange'])
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         site = Site(name='Site 1', slug='site-1')
         site.save()
 
         # Create three ObjectChanges
+        user = User.objects.create_user(username='testuser2')
         for i in range(1, 4):
             oc = site.to_objectchange(action=ObjectChangeActionChoices.ACTION_UPDATE)
             oc.user = user
@@ -96,10 +104,10 @@ def test_objectchange_list(self):
         }
 
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
 
     def test_objectchange(self):
 
         objectchange = ObjectChange.objects.first()
         response = self.client.get(objectchange.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py
--- a/netbox/ipam/tests/test_views.py
+++ b/netbox/ipam/tests/test_views.py
@@ -1,26 +1,18 @@
-from netaddr import IPNetwork
-import urllib.parse
+import datetime
 
-from django.test import Client, TestCase
-from django.urls import reverse
+from netaddr import IPNetwork
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
-from ipam.choices import ServiceProtocolChoices
+from ipam.choices import *
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
-from utilities.testing import create_test_user
+from utilities.testing import StandardTestCases
 
 
-class VRFTestCase(TestCase):
+class VRFTestCase(StandardTestCases.Views):
+    model = VRF
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_vrf',
-                'ipam.add_vrf',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         VRF.objects.bulk_create([
             VRF(name='VRF 1', rd='65000:1'),
@@ -28,48 +20,32 @@ def setUp(self):
             VRF(name='VRF 3', rd='65000:3'),
         ])
 
-    def test_vrf_list(self):
-
-        url = reverse('ipam:vrf_list')
-        params = {
-            "q": "65000",
+        cls.form_data = {
+            'name': 'VRF X',
+            'rd': '65000:999',
+            'tenant': None,
+            'enforce_unique': True,
+            'description': 'A new VRF',
+            'tags': 'Alpha,Bravo,Charlie',
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_vrf(self):
-
-        vrf = VRF.objects.first()
-        response = self.client.get(vrf.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_vrf_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "name",
             "VRF 4",
             "VRF 5",
             "VRF 6",
         )
 
-        response = self.client.post(reverse('ipam:vrf_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(VRF.objects.count(), 6)
+class RIRTestCase(StandardTestCases.Views):
+    model = RIR
 
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
 
-class RIRTestCase(TestCase):
-
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_rir',
-                'ipam.add_rir',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         RIR.objects.bulk_create([
             RIR(name='RIR 1', slug='rir-1'),
@@ -77,42 +53,27 @@ def setUp(self):
             RIR(name='RIR 3', slug='rir-3'),
         ])
 
-    def test_rir_list(self):
-
-        url = reverse('ipam:rir_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-
-    def test_rir_import(self):
+        cls.form_data = {
+            'name': 'RIR X',
+            'slug': 'rir-x',
+            'is_private': True,
+        }
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "RIR 4,rir-4",
             "RIR 5,rir-5",
             "RIR 6,rir-6",
         )
 
-        response = self.client.post(reverse('ipam:rir_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(RIR.objects.count(), 6)
 
+class AggregateTestCase(StandardTestCases.Views):
+    model = Aggregate
 
-class AggregateTestCase(TestCase):
-
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_aggregate',
-                'ipam.add_aggregate',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
-        rir = RIR(name='RIR 1', slug='rir-1')
-        rir.save()
+        rir = RIR.objects.create(name='RIR 1', slug='rir-1')
 
         Aggregate.objects.bulk_create([
             Aggregate(family=4, prefix=IPNetwork('10.1.0.0/16'), rir=rir),
@@ -120,48 +81,32 @@ def setUp(self):
             Aggregate(family=4, prefix=IPNetwork('10.3.0.0/16'), rir=rir),
         ])
 
-    def test_aggregate_list(self):
-
-        url = reverse('ipam:aggregate_list')
-        params = {
-            "rir": RIR.objects.first().slug,
+        cls.form_data = {
+            'family': 4,
+            'prefix': IPNetwork('10.99.0.0/16'),
+            'rir': rir.pk,
+            'date_added': datetime.date(2020, 1, 1),
+            'description': 'A new aggregate',
+            'tags': 'Alpha,Bravo,Charlie',
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_aggregate(self):
-
-        aggregate = Aggregate.objects.first()
-        response = self.client.get(aggregate.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_aggregate_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "prefix,rir",
             "10.4.0.0/16,RIR 1",
             "10.5.0.0/16,RIR 1",
             "10.6.0.0/16,RIR 1",
         )
 
-        response = self.client.post(reverse('ipam:aggregate_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Aggregate.objects.count(), 6)
 
+class RoleTestCase(StandardTestCases.Views):
+    model = Role
 
-class RoleTestCase(TestCase):
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_role',
-                'ipam.add_role',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         Role.objects.bulk_create([
             Role(name='Role 1', slug='role-1'),
@@ -169,42 +114,31 @@ def setUp(self):
             Role(name='Role 3', slug='role-3'),
         ])
 
-    def test_role_list(self):
-
-        url = reverse('ipam:role_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-
-    def test_role_import(self):
+        cls.form_data = {
+            'name': 'Role X',
+            'slug': 'role-x',
+            'weight': 200,
+            'description': 'A new role',
+        }
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug,weight",
             "Role 4,role-4,1000",
             "Role 5,role-5,1000",
             "Role 6,role-6,1000",
         )
 
-        response = self.client.post(reverse('ipam:role_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Role.objects.count(), 6)
 
+class PrefixTestCase(StandardTestCases.Views):
+    model = Prefix
 
-class PrefixTestCase(TestCase):
+    @classmethod
+    def setUpTestData(cls):
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_prefix',
-                'ipam.add_prefix',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
-
-        site = Site(name='Site 1', slug='site-1')
-        site.save()
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        vrf = VRF.objects.create(name='VRF 1', rd='65000:1')
+        role = Role.objects.create(name='Role 1', slug='role-1')
+        # vlan = VLAN.objects.create(vid=123, name='VLAN 123')
 
         Prefix.objects.bulk_create([
             Prefix(family=4, prefix=IPNetwork('10.1.0.0/16'), site=site),
@@ -212,51 +146,34 @@ def setUp(self):
             Prefix(family=4, prefix=IPNetwork('10.3.0.0/16'), site=site),
         ])
 
-    def test_prefix_list(self):
-
-        url = reverse('ipam:prefix_list')
-        params = {
-            "site": Site.objects.first().slug,
+        cls.form_data = {
+            'prefix': IPNetwork('192.0.2.0/24'),
+            'site': site.pk,
+            'vrf': vrf.pk,
+            'tenant': None,
+            'vlan': None,
+            'status': PrefixStatusChoices.STATUS_RESERVED,
+            'role': role.pk,
+            'is_pool': True,
+            'description': 'A new prefix',
+            'tags': 'Alpha,Bravo,Charlie',
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_prefix(self):
-
-        prefix = Prefix.objects.first()
-        response = self.client.get(prefix.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_prefix_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "prefix,status",
             "10.4.0.0/16,Active",
             "10.5.0.0/16,Active",
             "10.6.0.0/16,Active",
         )
 
-        response = self.client.post(reverse('ipam:prefix_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Prefix.objects.count(), 6)
 
+class IPAddressTestCase(StandardTestCases.Views):
+    model = IPAddress
 
-class IPAddressTestCase(TestCase):
+    @classmethod
+    def setUpTestData(cls):
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_ipaddress',
-                'ipam.add_ipaddress',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
-
-        vrf = VRF(name='VRF 1', rd='65000:1')
-        vrf.save()
+        vrf = VRF.objects.create(name='VRF 1', rd='65000:1')
 
         IPAddress.objects.bulk_create([
             IPAddress(family=4, address=IPNetwork('192.0.2.1/24'), vrf=vrf),
@@ -264,51 +181,38 @@ def setUp(self):
             IPAddress(family=4, address=IPNetwork('192.0.2.3/24'), vrf=vrf),
         ])
 
-    def test_ipaddress_list(self):
-
-        url = reverse('ipam:ipaddress_list')
-        params = {
-            "vrf": VRF.objects.first().rd,
+        cls.form_data = {
+            'vrf': vrf.pk,
+            'address': IPNetwork('192.0.2.99/24'),
+            'tenant': None,
+            'status': IPAddressStatusChoices.STATUS_RESERVED,
+            'role': IPAddressRoleChoices.ROLE_ANYCAST,
+            'interface': None,
+            'nat_inside': None,
+            'dns_name': 'example',
+            'description': 'A new IP address',
+            'tags': 'Alpha,Bravo,Charlie',
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_ipaddress(self):
-
-        ipaddress = IPAddress.objects.first()
-        response = self.client.get(ipaddress.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_ipaddress_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "address,status",
             "192.0.2.4/24,Active",
             "192.0.2.5/24,Active",
             "192.0.2.6/24,Active",
         )
 
-        response = self.client.post(reverse('ipam:ipaddress_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(IPAddress.objects.count(), 6)
 
+class VLANGroupTestCase(StandardTestCases.Views):
+    model = VLANGroup
 
-class VLANGroupTestCase(TestCase):
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_vlangroup',
-                'ipam.add_vlangroup',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
-        site = Site(name='Site 1', slug='site-1')
-        site.save()
+        site = Site.objects.create(name='Site 1', slug='site-1')
 
         VLANGroup.objects.bulk_create([
             VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=site),
@@ -316,45 +220,29 @@ def setUp(self):
             VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=site),
         ])
 
-    def test_vlangroup_list(self):
-
-        url = reverse('ipam:vlangroup_list')
-        params = {
-            "site": Site.objects.first().slug,
+        cls.form_data = {
+            'name': 'VLAN Group X',
+            'slug': 'vlan-group-x',
+            'site': site.pk,
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_vlangroup_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "VLAN Group 4,vlan-group-4",
             "VLAN Group 5,vlan-group-5",
             "VLAN Group 6,vlan-group-6",
         )
 
-        response = self.client.post(reverse('ipam:vlangroup_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(VLANGroup.objects.count(), 6)
+class VLANTestCase(StandardTestCases.Views):
+    model = VLAN
 
+    @classmethod
+    def setUpTestData(cls):
 
-class VLANTestCase(TestCase):
-
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_vlan',
-                'ipam.add_vlan',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
-
-        vlangroup = VLANGroup(name='VLAN Group 1', slug='vlan-group-1')
-        vlangroup.save()
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        vlangroup = VLANGroup.objects.create(name='VLAN Group 1', slug='vlan-group-1', site=site)
+        role = Role.objects.create(name='Role 1', slug='role-1')
 
         VLAN.objects.bulk_create([
             VLAN(group=vlangroup, vid=101, name='VLAN101'),
@@ -362,58 +250,43 @@ def setUp(self):
             VLAN(group=vlangroup, vid=103, name='VLAN103'),
         ])
 
-    def test_vlan_list(self):
-
-        url = reverse('ipam:vlan_list')
-        params = {
-            "group": VLANGroup.objects.first().slug,
+        cls.form_data = {
+            'site': site.pk,
+            'group': vlangroup.pk,
+            'vid': 999,
+            'name': 'VLAN999',
+            'tenant': None,
+            'status': VLANStatusChoices.STATUS_RESERVED,
+            'role': role.pk,
+            'description': 'A new VLAN',
+            'tags': 'Alpha,Bravo,Charlie',
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_vlan(self):
-
-        vlan = VLAN.objects.first()
-        response = self.client.get(vlan.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_vlan_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "vid,name,status",
             "104,VLAN104,Active",
             "105,VLAN105,Active",
             "106,VLAN106,Active",
         )
 
-        response = self.client.post(reverse('ipam:vlan_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(VLAN.objects.count(), 6)
+class ServiceTestCase(StandardTestCases.Views):
+    model = Service
 
+    # Disable inapplicable tests
+    test_import_objects = None
 
-class ServiceTestCase(TestCase):
+    # TODO: Resolve URL for Service creation
+    test_create_object = None
 
-    def setUp(self):
-        user = create_test_user(permissions=['ipam.view_service'])
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
-        site = Site(name='Site 1', slug='site-1')
-        site.save()
-
-        manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
-        manufacturer.save()
-
-        devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1')
-        devicetype.save()
-
-        devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
-        devicerole.save()
-
-        device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
-        device.save()
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
+        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
+        devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
+        device = Device.objects.create(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
 
         Service.objects.bulk_create([
             Service(device=device, name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=101),
@@ -421,18 +294,13 @@ def setUp(self):
             Service(device=device, name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=103),
         ])
 
-    def test_service_list(self):
-
-        url = reverse('ipam:service_list')
-        params = {
-            "device_id": Device.objects.first(),
+        cls.form_data = {
+            'device': device.pk,
+            'virtual_machine': None,
+            'name': 'Service X',
+            'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
+            'port': 999,
+            'ipaddresses': [],
+            'description': 'A new service',
+            'tags': 'Alpha,Bravo,Charlie',
         }
-
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_service(self):
-
-        service = Service.objects.first()
-        response = self.client.get(service.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
diff --git a/netbox/netbox/tests/test_views.py b/netbox/netbox/tests/test_views.py
--- a/netbox/netbox/tests/test_views.py
+++ b/netbox/netbox/tests/test_views.py
@@ -1,6 +1,6 @@
 import urllib.parse
 
-from django.test import TestCase
+from utilities.testing import TestCase
 from django.urls import reverse
 
 
@@ -11,7 +11,7 @@ def test_home(self):
         url = reverse('home')
 
         response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
 
     def test_search(self):
 
@@ -21,4 +21,4 @@ def test_search(self):
         }
 
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
diff --git a/netbox/secrets/tests/test_views.py b/netbox/secrets/tests/test_views.py
--- a/netbox/secrets/tests/test_views.py
+++ b/netbox/secrets/tests/test_views.py
@@ -1,26 +1,22 @@
 import base64
-import urllib.parse
 
-from django.test import Client, TestCase
 from django.urls import reverse
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from secrets.models import Secret, SecretRole, SessionKey, UserKey
-from utilities.testing import create_test_user
+from utilities.testing import StandardTestCases
 from .constants import PRIVATE_KEY, PUBLIC_KEY
 
 
-class SecretRoleTestCase(TestCase):
+class SecretRoleTestCase(StandardTestCases.Views):
+    model = SecretRole
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'secrets.view_secretrole',
-                'secrets.add_secretrole',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
+
+    @classmethod
+    def setUpTestData(cls):
 
         SecretRole.objects.bulk_create([
             SecretRole(name='Secret Role 1', slug='secret-role-1'),
@@ -28,65 +24,40 @@ def setUp(self):
             SecretRole(name='Secret Role 3', slug='secret-role-3'),
         ])
 
-    def test_secretrole_list(self):
-
-        url = reverse('secrets:secretrole_list')
-
-        response = self.client.get(url, follow=True)
-        self.assertEqual(response.status_code, 200)
-
-    def test_secretrole_import(self):
+        cls.form_data = {
+            'name': 'Secret Role X',
+            'slug': 'secret-role-x',
+            'description': 'A secret role',
+            'users': [],
+            'groups': [],
+        }
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "Secret Role 4,secret-role-4",
             "Secret Role 5,secret-role-5",
             "Secret Role 6,secret-role-6",
         )
 
-        response = self.client.post(reverse('secrets:secretrole_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(SecretRole.objects.count(), 6)
+class SecretTestCase(StandardTestCases.Views):
+    model = Secret
 
+    # Disable inapplicable tests
+    test_create_object = None
 
-class SecretTestCase(TestCase):
+    # TODO: Check permissions enforcement on secrets.views.secret_edit
+    test_edit_object = None
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'secrets.view_secret',
-                'secrets.add_secret',
-            ]
-        )
+    @classmethod
+    def setUpTestData(cls):
 
-        # Set up a master key
-        userkey = UserKey(user=user, public_key=PUBLIC_KEY)
-        userkey.save()
-        master_key = userkey.get_master_key(PRIVATE_KEY)
-        self.session_key = SessionKey(userkey=userkey)
-        self.session_key.save(master_key)
-
-        self.client = Client()
-        self.client.force_login(user)
-
-        site = Site(name='Site 1', slug='site-1')
-        site.save()
-
-        manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
-        manufacturer.save()
-
-        devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1')
-        devicetype.save()
-
-        devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
-        devicerole.save()
-
-        device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
-        device.save()
-
-        secretrole = SecretRole(name='Secret Role 1', slug='secret-role-1')
-        secretrole.save()
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
+        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
+        devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
+        device = Device.objects.create(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
+        secretrole = SecretRole.objects.create(name='Secret Role 1', slug='secret-role-1')
 
         Secret.objects.bulk_create([
             Secret(device=device, role=secretrole, name='Secret 1', ciphertext=b'1234567890'),
@@ -94,23 +65,25 @@ def setUp(self):
             Secret(device=device, role=secretrole, name='Secret 3', ciphertext=b'1234567890'),
         ])
 
-    def test_secret_list(self):
-
-        url = reverse('secrets:secret_list')
-        params = {
-            "role": SecretRole.objects.first().slug,
+        cls.form_data = {
+            'device': device.pk,
+            'role': secretrole.pk,
+            'name': 'Secret X',
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True)
-        self.assertEqual(response.status_code, 200)
+    def setUp(self):
 
-    def test_secret(self):
+        super().setUp()
 
-        secret = Secret.objects.first()
-        response = self.client.get(secret.get_absolute_url(), follow=True)
-        self.assertEqual(response.status_code, 200)
+        # Set up a master key for the test user
+        userkey = UserKey(user=self.user, public_key=PUBLIC_KEY)
+        userkey.save()
+        master_key = userkey.get_master_key(PRIVATE_KEY)
+        self.session_key = SessionKey(userkey=userkey)
+        self.session_key.save(master_key)
 
-    def test_secret_import(self):
+    def test_import_objects(self):
+        self.add_permissions('secrets.add_secret')
 
         csv_data = (
             "device,role,name,plaintext",
@@ -125,5 +98,5 @@ def test_secret_import(self):
 
         response = self.client.post(reverse('secrets:secret_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
         self.assertEqual(Secret.objects.count(), 6)
diff --git a/netbox/tenancy/tests/test_views.py b/netbox/tenancy/tests/test_views.py
--- a/netbox/tenancy/tests/test_views.py
+++ b/netbox/tenancy/tests/test_views.py
@@ -1,23 +1,16 @@
-import urllib.parse
-
-from django.test import Client, TestCase
-from django.urls import reverse
-
 from tenancy.models import Tenant, TenantGroup
-from utilities.testing import create_test_user
+from utilities.testing import StandardTestCases
 
 
-class TenantGroupTestCase(TestCase):
+class TenantGroupTestCase(StandardTestCases.Views):
+    model = TenantGroup
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'tenancy.view_tenantgroup',
-                'tenancy.add_tenantgroup',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
+
+    @classmethod
+    def setUpTestData(cls):
 
         TenantGroup.objects.bulk_create([
             TenantGroup(name='Tenant Group 1', slug='tenant-group-1'),
@@ -25,42 +18,26 @@ def setUp(self):
             TenantGroup(name='Tenant Group 3', slug='tenant-group-3'),
         ])
 
-    def test_tenantgroup_list(self):
-
-        url = reverse('tenancy:tenantgroup_list')
-
-        response = self.client.get(url, follow=True)
-        self.assertEqual(response.status_code, 200)
-
-    def test_tenantgroup_import(self):
+        cls.form_data = {
+            'name': 'Tenant Group X',
+            'slug': 'tenant-group-x',
+        }
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "Tenant Group 4,tenant-group-4",
             "Tenant Group 5,tenant-group-5",
             "Tenant Group 6,tenant-group-6",
         )
 
-        response = self.client.post(reverse('tenancy:tenantgroup_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(TenantGroup.objects.count(), 6)
 
+class TenantTestCase(StandardTestCases.Views):
+    model = Tenant
 
-class TenantTestCase(TestCase):
+    @classmethod
+    def setUpTestData(cls):
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'tenancy.view_tenant',
-                'tenancy.add_tenant',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
-
-        tenantgroup = TenantGroup(name='Tenant Group 1', slug='tenant-group-1')
-        tenantgroup.save()
+        tenantgroup = TenantGroup.objects.create(name='Tenant Group 1', slug='tenant-group-1')
 
         Tenant.objects.bulk_create([
             Tenant(name='Tenant 1', slug='tenant-1', group=tenantgroup),
@@ -68,32 +45,18 @@ def setUp(self):
             Tenant(name='Tenant 3', slug='tenant-3', group=tenantgroup),
         ])
 
-    def test_tenant_list(self):
-
-        url = reverse('tenancy:tenant_list')
-        params = {
-            "group": TenantGroup.objects.first().slug,
+        cls.form_data = {
+            'name': 'Tenant X',
+            'slug': 'tenant-x',
+            'group': tenantgroup.pk,
+            'description': 'A new tenant',
+            'comments': 'Some comments',
+            'tags': 'Alpha,Bravo,Charlie',
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True)
-        self.assertEqual(response.status_code, 200)
-
-    def test_tenant(self):
-
-        tenant = Tenant.objects.first()
-        response = self.client.get(tenant.get_absolute_url(), follow=True)
-        self.assertEqual(response.status_code, 200)
-
-    def test_tenant_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "Tenant 4,tenant-4",
             "Tenant 5,tenant-5",
             "Tenant 6,tenant-6",
         )
-
-        response = self.client.post(reverse('tenancy:tenant_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Tenant.objects.count(), 6)
diff --git a/netbox/utilities/testing.py b/netbox/utilities/testing.py
deleted file mode 100644
--- a/netbox/utilities/testing.py
+++ /dev/null
@@ -1,79 +0,0 @@
-import logging
-from contextlib import contextmanager
-
-from django.contrib.auth.models import Permission, User
-from rest_framework.test import APITestCase as _APITestCase
-
-from users.models import Token
-
-
-class APITestCase(_APITestCase):
-
-    def setUp(self):
-        """
-        Create a superuser and token for API calls.
-        """
-        self.user = User.objects.create(username='testuser', is_superuser=True)
-        self.token = Token.objects.create(user=self.user)
-        self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(self.token.key)}
-
-    def assertHttpStatus(self, response, expected_status):
-        """
-        Provide more detail in the event of an unexpected HTTP response.
-        """
-        err_message = "Expected HTTP status {}; received {}: {}"
-        self.assertEqual(response.status_code, expected_status, err_message.format(
-            expected_status, response.status_code, getattr(response, 'data', 'No data')
-        ))
-
-
-def create_test_user(username='testuser', permissions=list()):
-    """
-    Create a User with the given permissions.
-    """
-    user = User.objects.create_user(username=username)
-    for perm_name in permissions:
-        app, codename = perm_name.split('.')
-        perm = Permission.objects.get(content_type__app_label=app, codename=codename)
-        user.user_permissions.add(perm)
-
-    return user
-
-
-def choices_to_dict(choices_list):
-    """
-    Convert a list of field choices to a dictionary suitable for direct comparison with a ChoiceSet. For example:
-
-        [
-            {
-                "value": "choice-1",
-                "label": "First Choice"
-            },
-            {
-                "value": "choice-2",
-                "label": "Second Choice"
-            }
-        ]
-
-    Becomes:
-
-        {
-            "choice-1": "First Choice",
-            "choice-2": "Second Choice
-        }
-    """
-    return {
-        choice['value']: choice['label'] for choice in choices_list
-    }
-
-
-@contextmanager
-def disable_warnings(logger_name):
-    """
-    Temporarily suppress expected warning messages to keep the test output clean.
-    """
-    logger = logging.getLogger(logger_name)
-    current_level = logger.level
-    logger.setLevel(logging.ERROR)
-    yield
-    logger.setLevel(current_level)
diff --git a/netbox/utilities/testing/__init__.py b/netbox/utilities/testing/__init__.py
new file mode 100644
--- /dev/null
+++ b/netbox/utilities/testing/__init__.py
@@ -0,0 +1,2 @@
+from .testcases import *
+from .utils import *
diff --git a/netbox/utilities/testing/testcases.py b/netbox/utilities/testing/testcases.py
new file mode 100644
--- /dev/null
+++ b/netbox/utilities/testing/testcases.py
@@ -0,0 +1,249 @@
+from django.contrib.auth.models import Permission, User
+from django.core.exceptions import ObjectDoesNotExist
+from django.test import Client, TestCase as _TestCase, override_settings
+from django.urls import reverse, NoReverseMatch
+from rest_framework.test import APIClient
+
+from users.models import Token
+from .utils import disable_warnings, model_to_dict, post_data
+
+
+class TestCase(_TestCase):
+    user_permissions = ()
+
+    def setUp(self):
+
+        # Create the test user and assign permissions
+        self.user = User.objects.create_user(username='testuser')
+        self.add_permissions(*self.user_permissions)
+
+        # Initialize the test client
+        self.client = Client()
+        self.client.force_login(self.user)
+
+    #
+    # Permissions management
+    #
+
+    def add_permissions(self, *names):
+        """
+        Assign a set of permissions to the test user. Accepts permission names in the form <app>.<action>_<model>.
+        """
+        for name in names:
+            app, codename = name.split('.')
+            perm = Permission.objects.get(content_type__app_label=app, codename=codename)
+            self.user.user_permissions.add(perm)
+
+    def remove_permissions(self, *names):
+        """
+        Remove a set of permissions from the test user, if assigned.
+        """
+        for name in names:
+            app, codename = name.split('.')
+            perm = Permission.objects.get(content_type__app_label=app, codename=codename)
+            self.user.user_permissions.remove(perm)
+
+    #
+    # Convenience methods
+    #
+
+    def assertHttpStatus(self, response, expected_status):
+        """
+        TestCase method. Provide more detail in the event of an unexpected HTTP response.
+        """
+        err_message = "Expected HTTP status {}; received {}: {}"
+        self.assertEqual(response.status_code, expected_status, err_message.format(
+            expected_status, response.status_code, getattr(response, 'data', 'No data')
+        ))
+
+
+class APITestCase(TestCase):
+    client_class = APIClient
+
+    def setUp(self):
+        """
+        Create a superuser and token for API calls.
+        """
+        self.user = User.objects.create(username='testuser', is_superuser=True)
+        self.token = Token.objects.create(user=self.user)
+        self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(self.token.key)}
+
+
+class StandardTestCases:
+    """
+    We keep any TestCases with test_* methods inside a class to prevent unittest from trying to run them.
+    """
+
+    class Views(TestCase):
+        """
+        Stock TestCase suitable for testing all standard View functions:
+            - List objects
+            - View single object
+            - Create new object
+            - Modify existing object
+            - Delete existing object
+            - Import multiple new objects
+        """
+        model = None
+        form_data = {}
+        csv_data = {}
+
+        maxDiff = None
+
+        def __init__(self, *args, **kwargs):
+
+            super().__init__(*args, **kwargs)
+
+            if self.model is None:
+                raise Exception("Test case requires model to be defined")
+
+        def _get_url(self, action, instance=None):
+            """
+            Return the URL name for a specific action. An instance must be specified for
+            get/edit/delete views.
+            """
+            url_format = '{}:{}_{{}}'.format(
+                self.model._meta.app_label,
+                self.model._meta.model_name
+            )
+
+            if action in ('list', 'add', 'import'):
+                return reverse(url_format.format(action))
+
+            elif action in ('get', 'edit', 'delete'):
+                if instance is None:
+                    raise Exception("Resolving {} URL requires specifying an instance".format(action))
+                # Attempt to resolve using slug first
+                if hasattr(self.model, 'slug'):
+                    try:
+                        return reverse(url_format.format(action), kwargs={'slug': instance.slug})
+                    except NoReverseMatch:
+                        pass
+                return reverse(url_format.format(action), kwargs={'pk': instance.pk})
+
+            else:
+                raise Exception("Invalid action for URL resolution: {}".format(action))
+
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
+        def test_list_objects(self):
+            # Attempt to make the request without required permissions
+            with disable_warnings('django.request'):
+                self.assertHttpStatus(self.client.get(self._get_url('list')), 403)
+
+            # Assign the required permission and submit again
+            self.add_permissions(
+                '{}.view_{}'.format(self.model._meta.app_label, self.model._meta.model_name)
+            )
+            response = self.client.get(self._get_url('list'))
+            self.assertHttpStatus(response, 200)
+
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
+        def test_get_object(self):
+            instance = self.model.objects.first()
+
+            # Attempt to make the request without required permissions
+            with disable_warnings('django.request'):
+                self.assertHttpStatus(self.client.get(instance.get_absolute_url()), 403)
+
+            # Assign the required permission and submit again
+            self.add_permissions(
+                '{}.view_{}'.format(self.model._meta.app_label, self.model._meta.model_name)
+            )
+            response = self.client.get(instance.get_absolute_url())
+            self.assertHttpStatus(response, 200)
+
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
+        def test_create_object(self):
+            initial_count = self.model.objects.count()
+            request = {
+                'path': self._get_url('add'),
+                'data': post_data(self.form_data),
+                'follow': False,  # Do not follow 302 redirects
+            }
+
+            # Attempt to make the request without required permissions
+            with disable_warnings('django.request'):
+                self.assertHttpStatus(self.client.post(**request), 403)
+
+            # Assign the required permission and submit again
+            self.add_permissions(
+                '{}.add_{}'.format(self.model._meta.app_label, self.model._meta.model_name)
+            )
+            response = self.client.post(**request)
+            self.assertHttpStatus(response, 302)
+
+            self.assertEqual(initial_count + 1, self.model.objects.count())
+            instance = self.model.objects.order_by('-pk').first()
+            self.assertDictEqual(model_to_dict(instance), self.form_data)
+
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
+        def test_edit_object(self):
+            instance = self.model.objects.first()
+
+            request = {
+                'path': self._get_url('edit', instance),
+                'data': post_data(self.form_data),
+                'follow': False,  # Do not follow 302 redirects
+            }
+
+            # Attempt to make the request without required permissions
+            with disable_warnings('django.request'):
+                self.assertHttpStatus(self.client.post(**request), 403)
+
+            # Assign the required permission and submit again
+            self.add_permissions(
+                '{}.change_{}'.format(self.model._meta.app_label, self.model._meta.model_name)
+            )
+            response = self.client.post(**request)
+            self.assertHttpStatus(response, 302)
+
+            instance = self.model.objects.get(pk=instance.pk)
+            self.assertDictEqual(model_to_dict(instance), self.form_data)
+
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
+        def test_delete_object(self):
+            instance = self.model.objects.first()
+
+            request = {
+                'path': self._get_url('delete', instance),
+                'data': {'confirm': True},
+                'follow': False,  # Do not follow 302 redirects
+            }
+
+            # Attempt to make the request without required permissions
+            with disable_warnings('django.request'):
+                self.assertHttpStatus(self.client.post(**request), 403)
+
+            # Assign the required permission and submit again
+            self.add_permissions(
+                '{}.delete_{}'.format(self.model._meta.app_label, self.model._meta.model_name)
+            )
+            response = self.client.post(**request)
+            self.assertHttpStatus(response, 302)
+
+            with self.assertRaises(ObjectDoesNotExist):
+                self.model.objects.get(pk=instance.pk)
+
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
+        def test_import_objects(self):
+            initial_count = self.model.objects.count()
+            request = {
+                'path': self._get_url('import'),
+                'data': {
+                    'csv': '\n'.join(self.csv_data)
+                }
+            }
+
+            # Attempt to make the request without required permissions
+            with disable_warnings('django.request'):
+                self.assertHttpStatus(self.client.post(**request), 403)
+
+            # Assign the required permission and submit again
+            self.add_permissions(
+                '{}.view_{}'.format(self.model._meta.app_label, self.model._meta.model_name),
+                '{}.add_{}'.format(self.model._meta.app_label, self.model._meta.model_name)
+            )
+            response = self.client.post(**request)
+            self.assertHttpStatus(response, 200)
+
+            self.assertEqual(self.model.objects.count(), initial_count + len(self.csv_data) - 1)
diff --git a/netbox/utilities/testing/utils.py b/netbox/utilities/testing/utils.py
new file mode 100644
--- /dev/null
+++ b/netbox/utilities/testing/utils.py
@@ -0,0 +1,102 @@
+import logging
+from contextlib import contextmanager
+
+from django.contrib.auth.models import Permission, User
+from django.forms.models import model_to_dict as _model_to_dict
+
+
+def model_to_dict(instance, fields=None, exclude=None):
+    """
+    Customized wrapper for Django's built-in model_to_dict(). Does the following:
+      - Excludes the instance ID field
+      - Exclude any fields prepended with an underscore
+      - Convert any assigned tags to a comma-separated string
+    """
+    _exclude = ['id']
+    if exclude is not None:
+        _exclude += exclude
+
+    model_dict = _model_to_dict(instance, fields=fields, exclude=_exclude)
+
+    for key in list(model_dict.keys()):
+        if key.startswith('_'):
+            del model_dict[key]
+
+        # TODO: Differentiate between tags assigned to the instance and a M2M field for tags (ex: ConfigContext)
+        elif key == 'tags':
+            model_dict[key] = ','.join(sorted([tag.name for tag in model_dict['tags']]))
+
+        # Convert ManyToManyField to list of instance PKs
+        elif model_dict[key] and type(model_dict[key]) in (list, tuple) and hasattr(model_dict[key][0], 'pk'):
+            model_dict[key] = [obj.pk for obj in model_dict[key]]
+
+    return model_dict
+
+
+def post_data(data):
+    """
+    Take a dictionary of test data (suitable for comparison to an instance) and return a dict suitable for POSTing.
+    """
+    ret = {}
+
+    for key, value in data.items():
+        if value is None:
+            ret[key] = ''
+        elif type(value) in (list, tuple):
+            ret[key] = value
+        else:
+            ret[key] = str(value)
+
+    return ret
+
+
+def create_test_user(username='testuser', permissions=list()):
+    """
+    Create a User with the given permissions.
+    """
+    user = User.objects.create_user(username=username)
+    for perm_name in permissions:
+        app, codename = perm_name.split('.')
+        perm = Permission.objects.get(content_type__app_label=app, codename=codename)
+        user.user_permissions.add(perm)
+
+    return user
+
+
+def choices_to_dict(choices_list):
+    """
+    Convert a list of field choices to a dictionary suitable for direct comparison with a ChoiceSet. For example:
+
+        [
+            {
+                "value": "choice-1",
+                "label": "First Choice"
+            },
+            {
+                "value": "choice-2",
+                "label": "Second Choice"
+            }
+        ]
+
+    Becomes:
+
+        {
+            "choice-1": "First Choice",
+            "choice-2": "Second Choice
+        }
+    """
+    return {
+        choice['value']: choice['label'] for choice in choices_list
+    }
+
+
+@contextmanager
+def disable_warnings(logger_name):
+    """
+    Temporarily suppress expected warning messages to keep the test output clean.
+    """
+    logger = logging.getLogger(logger_name)
+    current_level = logger.level
+    logger.setLevel(logging.ERROR)
+    yield
+    logger.setLevel(current_level)
diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py
--- a/netbox/virtualization/tests/test_views.py
+++ b/netbox/virtualization/tests/test_views.py
@@ -1,23 +1,18 @@
-import urllib.parse
-
-from django.test import Client, TestCase
-from django.urls import reverse
-
-from utilities.testing import create_test_user
+from dcim.models import DeviceRole, Platform, Site
+from utilities.testing import StandardTestCases
+from virtualization.choices import *
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
 
-class ClusterGroupTestCase(TestCase):
+class ClusterGroupTestCase(StandardTestCases.Views):
+    model = ClusterGroup
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'virtualization.view_clustergroup',
-                'virtualization.add_clustergroup',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
+
+    @classmethod
+    def setUpTestData(cls):
 
         ClusterGroup.objects.bulk_create([
             ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'),
@@ -25,39 +20,28 @@ def setUp(self):
             ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'),
         ])
 
-    def test_clustergroup_list(self):
-
-        url = reverse('virtualization:clustergroup_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-
-    def test_clustergroup_import(self):
+        cls.form_data = {
+            'name': 'Cluster Group X',
+            'slug': 'cluster-group-x',
+        }
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "Cluster Group 4,cluster-group-4",
             "Cluster Group 5,cluster-group-5",
             "Cluster Group 6,cluster-group-6",
         )
 
-        response = self.client.post(reverse('virtualization:clustergroup_import'), {'csv': '\n'.join(csv_data)})
 
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(ClusterGroup.objects.count(), 6)
+class ClusterTypeTestCase(StandardTestCases.Views):
+    model = ClusterType
 
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
 
-class ClusterTypeTestCase(TestCase):
-
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'virtualization.view_clustertype',
-                'virtualization.add_clustertype',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
         ClusterType.objects.bulk_create([
             ClusterType(name='Cluster Type 1', slug='cluster-type-1'),
@@ -65,45 +49,28 @@ def setUp(self):
             ClusterType(name='Cluster Type 3', slug='cluster-type-3'),
         ])
 
-    def test_clustertype_list(self):
-
-        url = reverse('virtualization:clustertype_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-
-    def test_clustertype_import(self):
+        cls.form_data = {
+            'name': 'Cluster Type X',
+            'slug': 'cluster-type-x',
+        }
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "Cluster Type 4,cluster-type-4",
             "Cluster Type 5,cluster-type-5",
             "Cluster Type 6,cluster-type-6",
         )
 
-        response = self.client.post(reverse('virtualization:clustertype_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(ClusterType.objects.count(), 6)
-
-
-class ClusterTestCase(TestCase):
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'virtualization.view_cluster',
-                'virtualization.add_cluster',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+class ClusterTestCase(StandardTestCases.Views):
+    model = Cluster
 
-        clustergroup = ClusterGroup(name='Cluster Group 1', slug='cluster-group-1')
-        clustergroup.save()
+    @classmethod
+    def setUpTestData(cls):
 
-        clustertype = ClusterType(name='Cluster Type 1', slug='cluster-type-1')
-        clustertype.save()
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        clustergroup = ClusterGroup.objects.create(name='Cluster Group 1', slug='cluster-group-1')
+        clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
 
         Cluster.objects.bulk_create([
             Cluster(name='Cluster 1', group=clustergroup, type=clustertype),
@@ -111,55 +78,34 @@ def setUp(self):
             Cluster(name='Cluster 3', group=clustergroup, type=clustertype),
         ])
 
-    def test_cluster_list(self):
-
-        url = reverse('virtualization:cluster_list')
-        params = {
-            "group": ClusterGroup.objects.first().slug,
-            "type": ClusterType.objects.first().slug,
+        cls.form_data = {
+            'name': 'Cluster X',
+            'group': clustergroup.pk,
+            'type': clustertype.pk,
+            'tenant': None,
+            'site': site.pk,
+            'comments': 'Some comments',
+            'tags': 'Alpha,Bravo,Charlie',
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_cluster(self):
-
-        cluster = Cluster.objects.first()
-        response = self.client.get(cluster.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_cluster_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "name,type",
             "Cluster 4,Cluster Type 1",
             "Cluster 5,Cluster Type 1",
             "Cluster 6,Cluster Type 1",
         )
 
-        response = self.client.post(reverse('virtualization:cluster_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Cluster.objects.count(), 6)
 
+class VirtualMachineTestCase(StandardTestCases.Views):
+    model = VirtualMachine
 
-class VirtualMachineTestCase(TestCase):
-
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'virtualization.view_virtualmachine',
-                'virtualization.add_virtualmachine',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
-        clustertype = ClusterType(name='Cluster Type 1', slug='cluster-type-1')
-        clustertype.save()
-
-        cluster = Cluster(name='Cluster 1', type=clustertype)
-        cluster.save()
+        devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
+        platform = Platform.objects.create(name='Platform 1', slug='platform-1')
+        clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
+        cluster = Cluster.objects.create(name='Cluster 1', type=clustertype)
 
         VirtualMachine.objects.bulk_create([
             VirtualMachine(name='Virtual Machine 1', cluster=cluster),
@@ -167,32 +113,26 @@ def setUp(self):
             VirtualMachine(name='Virtual Machine 3', cluster=cluster),
         ])
 
-    def test_virtualmachine_list(self):
-
-        url = reverse('virtualization:virtualmachine_list')
-        params = {
-            "cluster_id": Cluster.objects.first().pk,
+        cls.form_data = {
+            'cluster': cluster.pk,
+            'tenant': None,
+            'platform': None,
+            'name': 'Virtual Machine X',
+            'status': VirtualMachineStatusChoices.STATUS_STAGED,
+            'role': devicerole.pk,
+            'primary_ip4': None,
+            'primary_ip6': None,
+            'vcpus': 4,
+            'memory': 32768,
+            'disk': 4000,
+            'comments': 'Some comments',
+            'tags': 'Alpha,Bravo,Charlie',
+            'local_context_data': None,
         }
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_virtualmachine(self):
-
-        virtualmachine = VirtualMachine.objects.first()
-        response = self.client.get(virtualmachine.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_virtualmachine_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "name,cluster",
             "Virtual Machine 4,Cluster 1",
             "Virtual Machine 5,Cluster 1",
             "Virtual Machine 6,Cluster 1",
         )
-
-        response = self.client.post(reverse('virtualization:virtualmachine_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(VirtualMachine.objects.count(), 6)