Просмотр исходного кода

Reorganize base TestCase classes

jeremystretch 4 лет назад
Родитель
Сommit
664ba55460

+ 4 - 4
netbox/circuits/tests/test_views.py

@@ -6,7 +6,7 @@ from django.urls import reverse
 from circuits.choices import *
 from circuits.models import *
 from dcim.models import Cable, Interface, Site
-from utilities.testing import ViewTestCases, create_test_device
+from utilities.testing import ViewTestCases, create_tags, create_test_device
 
 
 class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@@ -21,7 +21,7 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             Provider(name='Provider 3', slug='provider-3', asn=65003),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'name': 'Provider X',
@@ -106,7 +106,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'cid': 'Circuit X',
@@ -157,7 +157,7 @@ class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             ProviderNetwork(name='Provider Network 3', provider=providers[0]),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'name': 'Provider Network X',

+ 18 - 18
netbox/dcim/tests/test_views.py

@@ -12,7 +12,7 @@ from dcim.choices import *
 from dcim.constants import *
 from dcim.models import *
 from ipam.models import VLAN
-from utilities.testing import ViewTestCases, create_test_device
+from utilities.testing import ViewTestCases, create_tags, create_test_device
 
 
 class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
@@ -109,7 +109,7 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             Site(name='Site 3', slug='site-3', region=regions[0], group=groups[1]),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'name': 'Site X',
@@ -242,7 +242,7 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             RackReservation(rack=rack, user=user2, units=[7, 8, 9], description='Reservation 3'),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'rack': rack.pk,
@@ -298,7 +298,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             Rack(name='Rack 3', site=sites[0]),
         ))
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'name': 'Rack X',
@@ -413,7 +413,7 @@ class DeviceTypeTestCase(
             DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturers[0]),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'manufacturer': manufacturers[1].pk,
@@ -1021,7 +1021,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             Device(name='Device 3', site=sites[0], rack=racks[0], device_type=devicetypes[0], device_role=deviceroles[0], platform=platforms[0]),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'device_type': devicetypes[1].pk,
@@ -1201,7 +1201,7 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
             ConsolePort(device=device, name='Console Port 3'),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'device': device.pk,
@@ -1259,7 +1259,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
             ConsoleServerPort(device=device, name='Console Server Port 3'),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'device': device.pk,
@@ -1315,7 +1315,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
             PowerPort(device=device, name='Power Port 3'),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'device': device.pk,
@@ -1383,7 +1383,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
             PowerOutlet(device=device, name='Power Outlet 3', power_port=powerports[0]),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'device': device.pk,
@@ -1452,7 +1452,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
         )
         VLAN.objects.bulk_create(vlans)
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'device': device.pk,
@@ -1539,7 +1539,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
             FrontPort(device=device, name='Front Port 3', rear_port=rearports[2]),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'device': device.pk,
@@ -1600,7 +1600,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
             RearPort(device=device, name='Rear Port 3'),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'device': device.pk,
@@ -1661,7 +1661,7 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
             DeviceBay(device=device, name='Device Bay 3'),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'device': device.pk,
@@ -1701,7 +1701,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
         InventoryItem.objects.create(device=device, name='Inventory Item 2')
         InventoryItem.objects.create(device=device, name='Inventory Item 3')
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'device': device.pk,
@@ -1791,7 +1791,7 @@ class CableTestCase(
         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()
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         interface_ct = ContentType.objects.get_for_model(Interface)
         cls.form_data = {
@@ -1918,7 +1918,7 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             PowerPanel(site=sites[0], location=locations[0], name='Power Panel 3'),
         ))
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'site': sites[1].pk,
@@ -1966,7 +1966,7 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             PowerFeed(name='Power Feed 3', power_panel=powerpanels[0], rack=racks[0]),
         ))
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'name': 'Power Feed X',

+ 4 - 4
netbox/extras/tests/test_changelog.py

@@ -7,7 +7,7 @@ from dcim.models import Site
 from extras.choices import *
 from extras.models import CustomField, ObjectChange, Tag
 from utilities.testing import APITestCase
-from utilities.testing.utils import post_data
+from utilities.testing.utils import create_tags, post_data
 from utilities.testing.views import ModelViewTestCase
 
 
@@ -38,7 +38,7 @@ class ChangeLogViewTest(ModelViewTestCase):
         cf_select.content_types.set([ct])
 
     def test_create_object(self):
-        tags = self.create_tags('Tag 1', 'Tag 2')
+        tags = create_tags('Tag 1', 'Tag 2')
         form_data = {
             'name': 'Site 1',
             'slug': 'site-1',
@@ -72,7 +72,7 @@ class ChangeLogViewTest(ModelViewTestCase):
     def test_update_object(self):
         site = Site(name='Site 1', slug='site-1')
         site.save()
-        tags = self.create_tags('Tag 1', 'Tag 2', 'Tag 3')
+        tags = create_tags('Tag 1', 'Tag 2', 'Tag 3')
         site.tags.set('Tag 1', 'Tag 2')
 
         form_data = {
@@ -116,7 +116,7 @@ class ChangeLogViewTest(ModelViewTestCase):
             }
         )
         site.save()
-        self.create_tags('Tag 1', 'Tag 2')
+        create_tags('Tag 1', 'Tag 2')
         site.tags.set('Tag 1', 'Tag 2')
 
         request = {

+ 3 - 3
netbox/extras/tests/test_tags.py

@@ -2,7 +2,7 @@ from django.urls import reverse
 from rest_framework import status
 
 from dcim.models import Site
-from utilities.testing import APITestCase
+from utilities.testing import APITestCase, create_tags
 
 
 class TaggedItemTest(APITestCase):
@@ -10,7 +10,7 @@ class TaggedItemTest(APITestCase):
     Test the application of Tags to and item (a Site, for example) upon creation (POST) and modification (PATCH).
     """
     def test_create_tagged_item(self):
-        tags = self.create_tags("Foo", "Bar", "Baz")
+        tags = create_tags("Foo", "Bar", "Baz")
         data = {
             'name': 'Test Site',
             'slug': 'test-site',
@@ -37,7 +37,7 @@ class TaggedItemTest(APITestCase):
             slug='test-site'
         )
         site.tags.add("Foo", "Bar", "Baz")
-        self.create_tags("New Tag")
+        create_tags("New Tag")
         data = {
             'tags': [
                 {"name": "Foo"},

+ 8 - 8
netbox/ipam/tests/test_views.py

@@ -6,7 +6,7 @@ from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from ipam.choices import *
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
 from tenancy.models import Tenant
-from utilities.testing import ViewTestCases
+from utilities.testing import ViewTestCases, create_tags
 
 
 class VRFTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@@ -27,7 +27,7 @@ class VRFTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             VRF(name='VRF 3', rd='65000:3'),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'name': 'VRF X',
@@ -64,7 +64,7 @@ class RouteTargetTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         )
         Tenant.objects.bulk_create(tenants)
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         route_targets = (
             RouteTarget(name='65000:1001', tenant=tenants[0]),
@@ -141,7 +141,7 @@ class AggregateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             Aggregate(prefix=IPNetwork('10.3.0.0/16'), rir=rirs[0]),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'prefix': IPNetwork('10.99.0.0/16'),
@@ -226,7 +226,7 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             Prefix(prefix=IPNetwork('10.3.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'prefix': IPNetwork('192.0.2.0/24'),
@@ -277,7 +277,7 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             IPAddress(address=IPNetwork('192.0.2.3/24'), vrf=vrfs[0]),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'vrf': vrfs[1].pk,
@@ -374,7 +374,7 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             VLAN(group=vlangroups[0], vid=103, name='VLAN103', site=sites[0], role=roles[0]),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'site': sites[1].pk,
@@ -434,7 +434,7 @@ class ServiceTestCase(
             Service(device=device, name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[103]),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'device': device.pk,

+ 2 - 2
netbox/tenancy/tests/test_views.py

@@ -1,5 +1,5 @@
 from tenancy.models import Tenant, TenantGroup
-from utilities.testing import ViewTestCases
+from utilities.testing import ViewTestCases, create_tags
 
 
 class TenantGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
@@ -53,7 +53,7 @@ class TenantTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[0]),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'name': 'Tenant X',

+ 1 - 0
netbox/utilities/testing/__init__.py

@@ -1,3 +1,4 @@
 from .api import *
+from .base import *
 from .utils import *
 from .views import *

+ 1 - 1
netbox/utilities/testing/api.py

@@ -9,8 +9,8 @@ from rest_framework.test import APIClient
 from extras.choices import ObjectChangeActionChoices
 from extras.models import ObjectChange
 from users.models import ObjectPermission, Token
+from .base import ModelTestCase
 from .utils import disable_warnings
-from .views import ModelTestCase
 
 
 __all__ = (

+ 161 - 0
netbox/utilities/testing/base.py

@@ -0,0 +1,161 @@
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.postgres.fields import ArrayField
+from django.core.exceptions import FieldDoesNotExist
+from django.db.models import ManyToManyField
+from django.forms.models import model_to_dict
+from django.test import Client, TestCase as _TestCase
+from netaddr import IPNetwork
+from taggit.managers import TaggableManager
+
+from users.models import ObjectPermission
+from utilities.permissions import resolve_permission_ct
+from .utils import extract_form_failures
+
+__all__ = (
+    'ModelTestCase',
+    'TestCase',
+)
+
+
+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:
+            ct, action = resolve_permission_ct(name)
+            obj_perm = ObjectPermission(name=name, actions=[action])
+            obj_perm.save()
+            obj_perm.users.add(self.user)
+            obj_perm.object_types.add(ct)
+
+    #
+    # Custom assertions
+    #
+
+    def assertHttpStatus(self, response, expected_status):
+        """
+        TestCase method. Provide more detail in the event of an unexpected HTTP response.
+        """
+        err_message = None
+        # Construct an error message only if we know the test is going to fail
+        if response.status_code != expected_status:
+            if hasattr(response, 'data'):
+                # REST API response; pass the response data through directly
+                err = response.data
+            else:
+                # Attempt to extract form validation errors from the response HTML
+                form_errors = extract_form_failures(response.content)
+                err = form_errors or response.content or 'No data'
+            err_message = f"Expected HTTP status {expected_status}; received {response.status_code}: {err}"
+        self.assertEqual(response.status_code, expected_status, err_message)
+
+
+class ModelTestCase(TestCase):
+    """
+    Parent class for TestCases which deal with models.
+    """
+    model = None
+
+    def _get_queryset(self):
+        """
+        Return a base queryset suitable for use in test methods.
+        """
+        return self.model.objects.all()
+
+    def prepare_instance(self, instance):
+        """
+        Test cases can override this method to perform any necessary manipulation of an instance prior to its evaluation
+        against test data. For example, it can be used to decrypt a Secret's plaintext attribute.
+        """
+        return instance
+
+    def model_to_dict(self, instance, fields, api=False):
+        """
+        Return a dictionary representation of an instance.
+        """
+        # Prepare the instance and call Django's model_to_dict() to extract all fields
+        model_dict = model_to_dict(self.prepare_instance(instance), fields=fields)
+
+        # Map any additional (non-field) instance attributes that were specified
+        for attr in fields:
+            if hasattr(instance, attr) and attr not in model_dict:
+                model_dict[attr] = getattr(instance, attr)
+
+        for key, value in list(model_dict.items()):
+            try:
+                field = instance._meta.get_field(key)
+            except FieldDoesNotExist:
+                # Attribute is not a model field
+                continue
+
+            # Handle ManyToManyFields
+            if value and type(field) in (ManyToManyField, TaggableManager):
+
+                if field.related_model is ContentType:
+                    model_dict[key] = sorted([f'{ct.app_label}.{ct.model}' for ct in value])
+                else:
+                    model_dict[key] = sorted([obj.pk for obj in value])
+
+            if api:
+
+                # Replace ContentType numeric IDs with <app_label>.<model>
+                if type(getattr(instance, key)) is ContentType:
+                    ct = ContentType.objects.get(pk=value)
+                    model_dict[key] = f'{ct.app_label}.{ct.model}'
+
+                # Convert IPNetwork instances to strings
+                elif type(value) is IPNetwork:
+                    model_dict[key] = str(value)
+
+            else:
+
+                # Convert ArrayFields to CSV strings
+                if type(instance._meta.get_field(key)) is ArrayField:
+                    model_dict[key] = ','.join([str(v) for v in value])
+
+        return model_dict
+
+    #
+    # Custom assertions
+    #
+
+    def assertInstanceEqual(self, instance, data, exclude=None, api=False):
+        """
+        Compare a model instance to a dictionary, checking that its attribute values match those specified
+        in the dictionary.
+
+        :param instance: Python object instance
+        :param data: Dictionary of test data used to define the instance
+        :param exclude: List of fields to exclude from comparison (e.g. passwords, which get hashed)
+        :param api: Set to True is the data is a JSON representation of the instance
+        """
+        if exclude is None:
+            exclude = []
+
+        fields = [k for k in data.keys() if k not in exclude]
+        model_dict = self.model_to_dict(instance, fields=fields, api=api)
+
+        # Omit any dictionary keys which are not instance attributes or have been excluded
+        relevant_data = {
+            k: v for k, v in data.items() if hasattr(instance, k) and k not in exclude
+        }
+
+        self.assertDictEqual(model_dict, relevant_data)

+ 11 - 0
netbox/utilities/testing/utils.py

@@ -3,8 +3,10 @@ import re
 from contextlib import contextmanager
 
 from django.contrib.auth.models import Permission, User
+from django.utils.text import slugify
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
+from extras.models import Tag
 
 
 def post_data(data):
@@ -59,6 +61,15 @@ def create_test_user(username='testuser', permissions=None):
     return user
 
 
+def create_tags(*names):
+    """
+    Create and return a Tag instance for each name given.
+    """
+    tags = [Tag(name=name, slug=slugify(name)) for name in names]
+    Tag.objects.bulk_create(tags)
+    return tags
+
+
 def extract_form_failures(content):
     """
     Given raw HTML content from an HTTP response, return a list of form errors.

+ 5 - 168
netbox/utilities/testing/views.py

@@ -1,182 +1,20 @@
-from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
-from django.contrib.postgres.fields import ArrayField
-from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist
-from django.db.models import ManyToManyField
-from django.forms.models import model_to_dict
-from django.test import Client, TestCase as _TestCase, override_settings
+from django.core.exceptions import ObjectDoesNotExist
+from django.test import override_settings
 from django.urls import reverse
-from django.utils.text import slugify
-from netaddr import IPNetwork
-from taggit.managers import TaggableManager
 
 from extras.choices import ObjectChangeActionChoices
-from extras.models import ObjectChange, Tag
+from extras.models import ObjectChange
 from users.models import ObjectPermission
-from utilities.permissions import resolve_permission_ct
-from .utils import disable_warnings, extract_form_failures, post_data
-
+from .base import ModelTestCase
+from .utils import disable_warnings, post_data
 
 __all__ = (
-    'TestCase',
-    'ModelTestCase',
     'ModelViewTestCase',
     'ViewTestCases',
 )
 
 
-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)
-
-    def prepare_instance(self, instance):
-        """
-        Test cases can override this method to perform any necessary manipulation of an instance prior to its evaluation
-        against test data. For example, it can be used to decrypt a Secret's plaintext attribute.
-        """
-        return instance
-
-    def model_to_dict(self, instance, fields, api=False):
-        """
-        Return a dictionary representation of an instance.
-        """
-        # Prepare the instance and call Django's model_to_dict() to extract all fields
-        model_dict = model_to_dict(self.prepare_instance(instance), fields=fields)
-
-        # Map any additional (non-field) instance attributes that were specified
-        for attr in fields:
-            if hasattr(instance, attr) and attr not in model_dict:
-                model_dict[attr] = getattr(instance, attr)
-
-        for key, value in list(model_dict.items()):
-            try:
-                field = instance._meta.get_field(key)
-            except FieldDoesNotExist:
-                # Attribute is not a model field
-                continue
-
-            # Handle ManyToManyFields
-            if value and type(field) in (ManyToManyField, TaggableManager):
-
-                if field.related_model is ContentType:
-                    model_dict[key] = sorted([f'{ct.app_label}.{ct.model}' for ct in value])
-                else:
-                    model_dict[key] = sorted([obj.pk for obj in value])
-
-            if api:
-
-                # Replace ContentType numeric IDs with <app_label>.<model>
-                if type(getattr(instance, key)) is ContentType:
-                    ct = ContentType.objects.get(pk=value)
-                    model_dict[key] = f'{ct.app_label}.{ct.model}'
-
-                # Convert IPNetwork instances to strings
-                elif type(value) is IPNetwork:
-                    model_dict[key] = str(value)
-
-            else:
-
-                # Convert ArrayFields to CSV strings
-                if type(instance._meta.get_field(key)) is ArrayField:
-                    model_dict[key] = ','.join([str(v) for v in value])
-
-        return model_dict
-
-    #
-    # 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:
-            ct, action = resolve_permission_ct(name)
-            obj_perm = ObjectPermission(name=name, actions=[action])
-            obj_perm.save()
-            obj_perm.users.add(self.user)
-            obj_perm.object_types.add(ct)
-
-    #
-    # Custom assertions
-    #
-
-    def assertHttpStatus(self, response, expected_status):
-        """
-        TestCase method. Provide more detail in the event of an unexpected HTTP response.
-        """
-        err_message = None
-        # Construct an error message only if we know the test is going to fail
-        if response.status_code != expected_status:
-            if hasattr(response, 'data'):
-                # REST API response; pass the response data through directly
-                err = response.data
-            else:
-                # Attempt to extract form validation errors from the response HTML
-                form_errors = extract_form_failures(response.content)
-                err = form_errors or response.content or 'No data'
-            err_message = f"Expected HTTP status {expected_status}; received {response.status_code}: {err}"
-        self.assertEqual(response.status_code, expected_status, err_message)
-
-    def assertInstanceEqual(self, instance, data, exclude=None, api=False):
-        """
-        Compare a model instance to a dictionary, checking that its attribute values match those specified
-        in the dictionary.
-
-        :param instance: Python object instance
-        :param data: Dictionary of test data used to define the instance
-        :param exclude: List of fields to exclude from comparison (e.g. passwords, which get hashed)
-        :param api: Set to True is the data is a JSON representation of the instance
-        """
-        if exclude is None:
-            exclude = []
-
-        fields = [k for k in data.keys() if k not in exclude]
-        model_dict = self.model_to_dict(instance, fields=fields, api=api)
-
-        # Omit any dictionary keys which are not instance attributes or have been excluded
-        relevant_data = {
-            k: v for k, v in data.items() if hasattr(instance, k) and k not in exclude
-        }
-
-        self.assertDictEqual(model_dict, relevant_data)
-
-    #
-    # Convenience methods
-    #
-
-    @classmethod
-    def create_tags(cls, *names):
-        """
-        Create and return a Tag instance for each name given.
-        """
-        tags = [Tag(name=name, slug=slugify(name)) for name in names]
-        Tag.objects.bulk_create(tags)
-        return tags
-
-
-class ModelTestCase(TestCase):
-    """
-    Parent class for TestCases which deal with models.
-    """
-    model = None
-
-    def _get_queryset(self):
-        """
-        Return a base queryset suitable for use in test methods.
-        """
-        return self.model.objects.all()
-
-
 #
 # UI Tests
 #
@@ -185,7 +23,6 @@ class ModelViewTestCase(ModelTestCase):
     """
     Base TestCase for model views. Subclass to test individual views.
     """
-
     def _get_base_url(self):
         """
         Return the base format for a URL for the test's model. Override this to test for a model which belongs

+ 4 - 4
netbox/virtualization/tests/test_views.py

@@ -5,7 +5,7 @@ from netaddr import EUI
 from dcim.choices import InterfaceModeChoices
 from dcim.models import DeviceRole, Platform, Site
 from ipam.models import VLAN
-from utilities.testing import ViewTestCases
+from utilities.testing import ViewTestCases, create_tags
 from virtualization.choices import *
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
 
@@ -100,7 +100,7 @@ class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             Cluster(name='Cluster 3', group=clustergroups[0], type=clustertypes[0], site=sites[0]),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'name': 'Cluster X',
@@ -174,7 +174,7 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             VirtualMachine(name='Virtual Machine 3', cluster=clusters[0], role=deviceroles[0], platform=platforms[0]),
         ])
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'cluster': clusters[1].pk,
@@ -256,7 +256,7 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
         )
         VLAN.objects.bulk_create(vlans)
 
-        tags = cls.create_tags('Alpha', 'Bravo', 'Charlie')
+        tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
             'virtual_machine': virtualmachines[1].pk,