diff --git a/django/contrib/gis/db/backends/base.py b/django/contrib/gis/db/backends/base.py
index 20d81d5de4..09d8452212 100644
--- a/django/contrib/gis/db/backends/base.py
+++ b/django/contrib/gis/db/backends/base.py
@@ -16,6 +16,9 @@ class BaseSpatialFeatures(object):
     # Does the database contain a SpatialRefSys model to store SRID information?
     has_spatialrefsys_table = True
 
+    # Reference implementation of 3D functions is:
+    # http://postgis.net/docs/PostGIS_Special_Functions_Index.html#PostGIS_3D_Functions
+    supports_3d_functions = False
     # Does the database support SRID transform operations?
     supports_transform = True
     # Do geometric relationship operations operate on real shapes (or only on bounding boxes)?
@@ -29,24 +32,34 @@ class BaseSpatialFeatures(object):
 
     # The following properties indicate if the database backend support
     # certain lookups (dwithin, left and right, relate, ...)
+    supports_distances_lookups = True
     supports_left_right_lookups = False
 
     @property
-    def supports_relate_lookup(self):
-        return 'relate' in self.connection.ops.geometry_functions
+    def supports_bbcontains_lookup(self):
+        return 'bbcontains' in self.connection.ops.gis_terms
 
     @property
-    def has_dwithin_lookup(self):
+    def supports_contained_lookup(self):
+        return 'contained' in self.connection.ops.gis_terms
+
+    @property
+    def supports_dwithin_lookup(self):
         return 'dwithin' in self.connection.ops.distance_functions
 
+    @property
+    def supports_relate_lookup(self):
+        return 'relate' in self.connection.ops.gis_terms
+
     # For each of those methods, the class will have a property named
     # `has_<name>_method` (defined in __init__) which accesses connection.ops
     # to determine GIS method availability.
     geoqueryset_methods = (
-        'centroid', 'difference', 'envelope', 'force_rhr', 'geohash', 'gml',
-        'intersection', 'kml', 'num_geom', 'perimeter', 'point_on_surface',
-        'reverse', 'scale', 'snap_to_grid', 'svg', 'sym_difference',
-        'transform', 'translate', 'union', 'unionagg',
+        'area', 'centroid', 'difference', 'distance', 'distance_spheroid',
+        'envelope', 'force_rhr', 'geohash', 'gml', 'intersection', 'kml',
+        'length', 'num_geom', 'perimeter', 'point_on_surface', 'reverse',
+        'scale', 'snap_to_grid', 'svg', 'sym_difference', 'transform',
+        'translate', 'union', 'unionagg',
     )
 
     # Specifies whether the Collect and Extent aggregates are supported by the database
diff --git a/django/contrib/gis/db/backends/mysql/base.py b/django/contrib/gis/db/backends/mysql/base.py
index 47e8177da8..769d2fa609 100644
--- a/django/contrib/gis/db/backends/mysql/base.py
+++ b/django/contrib/gis/db/backends/mysql/base.py
@@ -10,6 +10,7 @@ from django.contrib.gis.db.backends.mysql.operations import MySQLOperations
 
 class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures):
     has_spatialrefsys_table = False
+    supports_distances_lookups = False
     supports_transform = False
     supports_real_shape_operations = False
     supports_null_geometries = False
diff --git a/django/contrib/gis/db/backends/postgis/base.py b/django/contrib/gis/db/backends/postgis/base.py
index cb2cbe53aa..7a2a374f10 100644
--- a/django/contrib/gis/db/backends/postgis/base.py
+++ b/django/contrib/gis/db/backends/postgis/base.py
@@ -11,6 +11,7 @@ from django.contrib.gis.db.backends.postgis.schema import PostGISSchemaEditor
 
 
 class DatabaseFeatures(BaseSpatialFeatures, Psycopg2DatabaseFeatures):
+    supports_3d_functions = True
     supports_left_right_lookups = True
 
 
diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py
index a9a7bb7f6f..eb1d9fdeb4 100644
--- a/django/contrib/gis/db/models/query.py
+++ b/django/contrib/gis/db/models/query.py
@@ -631,8 +631,8 @@ class GeoQuerySet(QuerySet):
                 u, unit_name, s = get_srid_info(self.query.transformed_srid, connection)
                 geodetic = unit_name.lower() in geo_field.geodetic_units
 
-            if backend.spatialite and geodetic:
-                raise ValueError('SQLite does not support linear distance calculations on geodetic coordinate systems.')
+            if geodetic and not connection.features.supports_distance_geodetic:
+                raise ValueError('This database does not support linear distance calculations on geodetic coordinate systems.')
 
             if distance:
                 if self.query.transformed_srid:
@@ -690,8 +690,8 @@ class GeoQuerySet(QuerySet):
                     # works on 3D geometries.
                     procedure_fmt += ",'%(spheroid)s'"
                     procedure_args.update({'function': backend.length_spheroid, 'spheroid': params[1]})
-                elif geom_3d and backend.postgis:
-                    # Use 3D variants of perimeter and length routines on PostGIS.
+                elif geom_3d and connection.features.supports_3d_functions:
+                    # Use 3D variants of perimeter and length routines on supported backends.
                     if perimeter:
                         procedure_args.update({'function': backend.perimeter3d})
                     elif length:
diff --git a/django/contrib/gis/tests/distapp/models.py b/django/contrib/gis/tests/distapp/models.py
index 4d95b219d7..87694a424f 100644
--- a/django/contrib/gis/tests/distapp/models.py
+++ b/django/contrib/gis/tests/distapp/models.py
@@ -1,4 +1,5 @@
 from django.contrib.gis.db import models
+from django.contrib.gis.tests.utils import gisfield_may_be_null
 from django.utils.encoding import python_2_unicode_compatible
 
 
@@ -38,7 +39,7 @@ class CensusZipcode(NamedModel):
 
 class SouthTexasZipcode(NamedModel):
     "Model for a few South Texas ZIP codes."
-    poly = models.PolygonField(srid=32140, null=True)
+    poly = models.PolygonField(srid=32140, null=gisfield_may_be_null)
 
 
 class Interstate(NamedModel):
diff --git a/django/contrib/gis/tests/distapp/tests.py b/django/contrib/gis/tests/distapp/tests.py
index 348fe6f6cc..00b9ee1720 100644
--- a/django/contrib/gis/tests/distapp/tests.py
+++ b/django/contrib/gis/tests/distapp/tests.py
@@ -6,9 +6,7 @@ from django.db import connection
 from django.db.models import Q
 from django.contrib.gis.geos import HAS_GEOS
 from django.contrib.gis.measure import D  # alias for Distance
-from django.contrib.gis.tests.utils import (
-    mysql, oracle, postgis, spatialite, no_oracle
-)
+from django.contrib.gis.tests.utils import oracle, postgis, spatialite, no_oracle
 from django.test import TestCase, skipUnlessDBFeature
 
 if HAS_GEOS:
@@ -18,8 +16,6 @@ if HAS_GEOS:
         SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode)
 
 
-@skipUnless(HAS_GEOS and not mysql,
-    "GEOS and spatial db (not mysql) are required.")
 @skipUnlessDBFeature("gis_enabled")
 class DistanceTest(TestCase):
     fixtures = ['initial']
@@ -50,7 +46,7 @@ class DistanceTest(TestCase):
         self.assertEqual(1, Interstate.objects.count())
         self.assertEqual(1, SouthTexasInterstate.objects.count())
 
-    @skipUnlessDBFeature("has_dwithin_lookup")
+    @skipUnlessDBFeature("supports_dwithin_lookup")
     def test_dwithin(self):
         """
         Test the `dwithin` lookup type.
@@ -99,6 +95,7 @@ class DistanceTest(TestCase):
             else:
                 self.assertListEqual(au_cities, self.get_names(qs.filter(point__dwithin=(self.au_pnt, dist))))
 
+    @skipUnlessDBFeature("has_distance_method")
     def test_distance_projected(self):
         """
         Test the `distance` GeoQuerySet method on projected coordinate systems.
@@ -139,7 +136,7 @@ class DistanceTest(TestCase):
                 self.assertAlmostEqual(m_distances[i], c.distance.m, tol)
                 self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol)
 
-    @skipUnlessDBFeature("supports_distance_geodetic")
+    @skipUnlessDBFeature("has_distance_method", "supports_distance_geodetic")
     def test_distance_geodetic(self):
         """
         Test the `distance` GeoQuerySet method on geodetic coordinate systems.
@@ -149,16 +146,16 @@ class DistanceTest(TestCase):
         # Testing geodetic distance calculation with a non-point geometry
         # (a LineString of Wollongong and Shellharbour coords).
         ls = LineString(((150.902, -34.4245), (150.87, -34.5789)))
-        if oracle or postgis:
-            # Reference query:
-            #  SELECT ST_distance_sphere(point, ST_GeomFromText('LINESTRING(150.9020 -34.4245,150.8700 -34.5789)', 4326)) FROM distapp_australiacity ORDER BY name;
-            distances = [1120954.92533513, 140575.720018241, 640396.662906304,
-                         60580.9693849269, 972807.955955075, 568451.8357838,
-                         40435.4335201384, 0, 68272.3896586844, 12375.0643697706, 0]
-            qs = AustraliaCity.objects.distance(ls).order_by('name')
-            for city, distance in zip(qs, distances):
-                # Testing equivalence to within a meter.
-                self.assertAlmostEqual(distance, city.distance.m, 0)
+
+        # Reference query:
+        #  SELECT ST_distance_sphere(point, ST_GeomFromText('LINESTRING(150.9020 -34.4245,150.8700 -34.5789)', 4326)) FROM distapp_australiacity ORDER BY name;
+        distances = [1120954.92533513, 140575.720018241, 640396.662906304,
+                     60580.9693849269, 972807.955955075, 568451.8357838,
+                     40435.4335201384, 0, 68272.3896586844, 12375.0643697706, 0]
+        qs = AustraliaCity.objects.distance(ls).order_by('name')
+        for city, distance in zip(qs, distances):
+            # Testing equivalence to within a meter.
+            self.assertAlmostEqual(distance, city.distance.m, 0)
 
         # Got the reference distances using the raw SQL statements:
         #  SELECT ST_distance_spheroid(point, ST_GeomFromText('POINT(151.231341 -33.952685)', 4326), 'SPHEROID["WGS 84",6378137.0,298.257223563]') FROM distapp_australiacity WHERE (NOT (id = 11));
@@ -197,6 +194,7 @@ class DistanceTest(TestCase):
                 self.assertAlmostEqual(sphere_distances[i], c.distance.m, tol)
 
     @no_oracle  # Oracle already handles geographic distance calculation.
+    @skipUnlessDBFeature("has_distance_method")
     def test_distance_transform(self):
         """
         Test the `distance` GeoQuerySet method used with `transform` on a geographic field.
@@ -226,6 +224,7 @@ class DistanceTest(TestCase):
             for i, z in enumerate(qs):
                 self.assertAlmostEqual(z.distance.m, dists_m[i], 5)
 
+    @skipUnlessDBFeature("supports_distances_lookups")
     def test_distance_lookups(self):
         """
         Test the `distance_lt`, `distance_gt`, `distance_lte`, and `distance_gte` lookup types.
@@ -255,6 +254,7 @@ class DistanceTest(TestCase):
         qs = SouthTexasZipcode.objects.exclude(name='77005').filter(poly__distance_lte=(z.poly, D(m=300)))
         self.assertEqual(['77002', '77025', '77401'], self.get_names(qs))
 
+    @skipUnlessDBFeature("supports_distances_lookups", "supports_distance_geodetic")
     def test_geodetic_distance_lookups(self):
         """
         Test distance lookups on geodetic coordinate systems.
@@ -264,23 +264,11 @@ class DistanceTest(TestCase):
         line = GEOSGeometry('LINESTRING(144.9630 -37.8143,151.2607 -33.8870)', 4326)
         dist_qs = AustraliaCity.objects.filter(point__distance_lte=(line, D(km=100)))
 
-        if oracle or postgis:
-            # Oracle and PostGIS can do distance lookups on arbitrary geometries.
-            self.assertEqual(9, dist_qs.count())
-            self.assertEqual(['Batemans Bay', 'Canberra', 'Hillsdale',
-                              'Melbourne', 'Mittagong', 'Shellharbour',
-                              'Sydney', 'Thirroul', 'Wollongong'],
-                             self.get_names(dist_qs))
-        else:
-            # spatialite only allow geodetic distance queries (utilizing
-            # ST_Distance_Sphere/ST_Distance_Spheroid) from Points to PointFields
-            # on geometry columns.
-            self.assertRaises(ValueError, dist_qs.count)
-
-            # Ensured that a ValueError was raised, none of the rest of the test is
-            # support on this backend, so bail now.
-            if spatialite:
-                return
+        self.assertEqual(9, dist_qs.count())
+        self.assertEqual(['Batemans Bay', 'Canberra', 'Hillsdale',
+                          'Melbourne', 'Mittagong', 'Shellharbour',
+                          'Sydney', 'Thirroul', 'Wollongong'],
+                         self.get_names(dist_qs))
 
         # Too many params (4 in this case) should raise a ValueError.
         self.assertRaises(ValueError, len,
@@ -309,18 +297,18 @@ class DistanceTest(TestCase):
         # Geodetic distance lookup but telling GeoDjango to use `distance_spheroid`
         # instead (we should get the same results b/c accuracy variance won't matter
         # in this test case).
-        if postgis:
+        querysets = [qs1]
+        if connection.features.has_distance_spheroid_method:
             gq3 = Q(point__distance_lte=(wollongong.point, d1, 'spheroid'))
             gq4 = Q(point__distance_gte=(wollongong.point, d2, 'spheroid'))
             qs2 = AustraliaCity.objects.exclude(name='Wollongong').filter(gq3 | gq4)
-            querysets = [qs1, qs2]
-        else:
-            querysets = [qs1]
+            querysets.append(qs2)
 
         for qs in querysets:
             cities = self.get_names(qs)
             self.assertEqual(cities, ['Adelaide', 'Hobart', 'Shellharbour', 'Thirroul'])
 
+    @skipUnlessDBFeature("has_area_method")
     def test_area(self):
         """
         Test the `area` GeoQuerySet method.
@@ -333,6 +321,7 @@ class DistanceTest(TestCase):
         for i, z in enumerate(SouthTexasZipcode.objects.order_by('name').area()):
             self.assertAlmostEqual(area_sq_m[i], z.area.sq_m, tol)
 
+    @skipUnlessDBFeature("has_length_method")
     def test_length(self):
         """
         Test the `length` GeoQuerySet method.
@@ -342,13 +331,13 @@ class DistanceTest(TestCase):
         len_m1 = 473504.769553813
         len_m2 = 4617.668
 
-        if spatialite:
-            # Does not support geodetic coordinate systems.
-            self.assertRaises(ValueError, Interstate.objects.length)
-        else:
+        if connection.features.supports_distance_geodetic:
             qs = Interstate.objects.length()
             tol = 2 if oracle else 3
             self.assertAlmostEqual(len_m1, qs[0].length.m, tol)
+        else:
+            # Does not support geodetic coordinate systems.
+            self.assertRaises(ValueError, Interstate.objects.length)
 
         # Now doing length on a projected coordinate system.
         i10 = SouthTexasInterstate.objects.length().get(name='I-10')
@@ -370,6 +359,7 @@ class DistanceTest(TestCase):
         for i, c in enumerate(SouthTexasCity.objects.perimeter(model_att='perim')):
             self.assertEqual(0, c.perim.m)
 
+    @skipUnlessDBFeature("has_area_method", "has_distance_method")
     def test_measurement_null_fields(self):
         """
         Test the measurement GeoQuerySet methods on fields with NULL values.
diff --git a/django/contrib/gis/tests/geo3d/tests.py b/django/contrib/gis/tests/geo3d/tests.py
index 130124c00c..b57b130704 100644
--- a/django/contrib/gis/tests/geo3d/tests.py
+++ b/django/contrib/gis/tests/geo3d/tests.py
@@ -6,8 +6,7 @@ from unittest import skipUnless
 
 from django.contrib.gis.gdal import HAS_GDAL
 from django.contrib.gis.geos import HAS_GEOS
-from django.contrib.gis.tests.utils import postgis
-from django.test import TestCase
+from django.test import TestCase, skipUnlessDBFeature
 from django.utils._os import upath
 
 if HAS_GEOS:
@@ -62,7 +61,8 @@ bbox_data = (
 )
 
 
-@skipUnless(HAS_GEOS and HAS_GDAL and postgis, "Geos, GDAL and postgis are required.")
+@skipUnless(HAS_GDAL, "GDAL is required for Geo3DTest.")
+@skipUnlessDBFeature("gis_enabled", "supports_3d_functions")
 class Geo3DTest(TestCase):
     """
     Only a subset of the PostGIS routines are 3D-enabled, and this TestCase
@@ -70,7 +70,7 @@ class Geo3DTest(TestCase):
     available within GeoDjango.  For more information, see the PostGIS docs
     on the routines that support 3D:
 
-    http://postgis.refractions.net/documentation/manual-1.5/ch08.html#PostGIS_3D_Functions
+    http://postgis.net/docs/PostGIS_Special_Functions_Index.html#PostGIS_3D_Functions
     """
 
     def _load_interstate_data(self):
diff --git a/django/contrib/gis/tests/geoapp/models.py b/django/contrib/gis/tests/geoapp/models.py
index 1e07e00d9d..415dd568ad 100644
--- a/django/contrib/gis/tests/geoapp/models.py
+++ b/django/contrib/gis/tests/geoapp/models.py
@@ -1,10 +1,7 @@
 from django.contrib.gis.db import models
-from django.contrib.gis.tests.utils import mysql
+from django.contrib.gis.tests.utils import gisfield_may_be_null
 from django.utils.encoding import python_2_unicode_compatible
 
-# MySQL spatial indices can't handle NULL geometries.
-null_flag = not mysql
-
 
 @python_2_unicode_compatible
 class NamedModel(models.Model):
@@ -42,7 +39,7 @@ class PennsylvaniaCity(City):
 
 
 class State(NamedModel):
-    poly = models.PolygonField(null=null_flag)  # Allowing NULL geometries here.
+    poly = models.PolygonField(null=gisfield_may_be_null)  # Allowing NULL geometries here.
 
 
 class Track(NamedModel):
diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py
index 87dd30a812..8fd912fb9e 100644
--- a/django/contrib/gis/tests/geoapp/tests.py
+++ b/django/contrib/gis/tests/geoapp/tests.py
@@ -2,12 +2,11 @@ from __future__ import unicode_literals
 
 import re
 import unittest
-from unittest import skipUnless
 
 from django.db import connection
 from django.contrib.gis import gdal
 from django.contrib.gis.geos import HAS_GEOS
-from django.contrib.gis.tests.utils import mysql, oracle, postgis, spatialite
+from django.contrib.gis.tests.utils import oracle, postgis, spatialite
 from django.test import TestCase, skipUnlessDBFeature
 from django.utils import six
 
@@ -142,11 +141,12 @@ class GeoModelTest(TestCase):
 
         # If the GeometryField SRID is -1, then we shouldn't perform any
         # transformation if the SRID of the input geometry is different.
-        # SpatiaLite does not support missing SRID values.
-        if not spatialite:
-            m1 = MinusOneSRID(geom=Point(17, 23, srid=4326))
-            m1.save()
-            self.assertEqual(-1, m1.geom.srid)
+        if spatialite and connection.ops.spatial_version < 3:
+            # SpatiaLite < 3 does not support missing SRID values.
+            return
+        m1 = MinusOneSRID(geom=Point(17, 23, srid=4326))
+        m1.save()
+        self.assertEqual(-1, m1.geom.srid)
 
     def test_createnull(self):
         "Testing creating a model instance and the geometry being None"
@@ -223,7 +223,7 @@ class GeoLookupTest(TestCase):
         # Seeing what cities are in Texas, should get Houston and Dallas,
         #  and Oklahoma City because 'contained' only checks on the
         #  _bounding box_ of the Geometries.
-        if not oracle:
+        if connection.features.supports_contained_lookup:
             qs = City.objects.filter(point__contained=texas.mpoly)
             self.assertEqual(3, qs.count())
             cities = ['Houston', 'Dallas', 'Oklahoma City']
@@ -245,23 +245,22 @@ class GeoLookupTest(TestCase):
         self.assertEqual('New Zealand', nz.name)
 
         # Spatialite 2.3 thinks that Lawrence is in Puerto Rico (a NULL geometry).
-        if not spatialite:
+        if not (spatialite and connection.ops.spatial_version < 3):
             ks = State.objects.get(poly__contains=lawrence.point)
             self.assertEqual('Kansas', ks.name)
 
         # Pueblo and Oklahoma City (even though OK City is within the bounding box of Texas)
         # are not contained in Texas or New Zealand.
-        self.assertEqual(0, len(Country.objects.filter(mpoly__contains=pueblo.point)))  # Query w/GEOSGeometry object
-        self.assertEqual((mysql and 1) or 0,
-                         len(Country.objects.filter(mpoly__contains=okcity.point.wkt)))  # Qeury w/WKT
+        self.assertEqual(len(Country.objects.filter(mpoly__contains=pueblo.point)), 0)  # Query w/GEOSGeometry object
+        self.assertEqual(len(Country.objects.filter(mpoly__contains=okcity.point.wkt)),
+                         0 if connection.features.supports_real_shape_operations else 1)  # Query w/WKT
 
         # OK City is contained w/in bounding box of Texas.
-        if not oracle:
+        if connection.features.supports_bbcontains_lookup:
             qs = Country.objects.filter(mpoly__bbcontains=okcity.point)
             self.assertEqual(1, len(qs))
             self.assertEqual('Texas', qs[0].name)
 
-    # Only PostGIS has `left` and `right` lookup types.
     @skipUnlessDBFeature("supports_left_right_lookups")
     def test_left_right_lookups(self):
         "Testing the 'left' and 'right' lookup types."
@@ -409,10 +408,9 @@ class GeoQuerySetTest(TestCase):
         for s in qs:
             self.assertEqual(True, s.poly.centroid.equals_exact(s.centroid, tol))
 
-    @skipUnlessDBFeature("has_difference_method")
-    @skipUnlessDBFeature("has_intersection_method")
-    @skipUnlessDBFeature("has_sym_difference_method")
-    @skipUnlessDBFeature("has_union_method")
+    @skipUnlessDBFeature(
+        "has_difference_method", "has_intersection_method",
+        "has_sym_difference_method", "has_union_method")
     def test_diff_intersection_union(self):
         "Testing the `difference`, `intersection`, `sym_difference`, and `union` GeoQuerySet methods."
         geom = Point(5, 23)
@@ -610,7 +608,7 @@ class GeoQuerySetTest(TestCase):
                    'Texas': fromstr('POINT (-103.002434 36.500397)', srid=4326),
                    }
 
-        elif postgis or spatialite:
+        else:
             # Using GEOSGeometry to compute the reference point on surface values
             # -- since PostGIS also uses GEOS these should be the same.
             ref = {'New Zealand': Country.objects.get(name='New Zealand').mpoly.point_on_surface,
diff --git a/django/contrib/gis/tests/geogapp/tests.py b/django/contrib/gis/tests/geogapp/tests.py
index 3d73e69867..59851c35a0 100644
--- a/django/contrib/gis/tests/geogapp/tests.py
+++ b/django/contrib/gis/tests/geogapp/tests.py
@@ -10,14 +10,14 @@ from django.contrib.gis.gdal import HAS_GDAL
 from django.contrib.gis.geos import HAS_GEOS
 from django.contrib.gis.measure import D
 from django.contrib.gis.tests.utils import postgis
-from django.test import TestCase
+from django.test import TestCase, skipUnlessDBFeature
 from django.utils._os import upath
 
 if HAS_GEOS:
     from .models import City, County, Zipcode
 
 
-@skipUnless(HAS_GEOS and postgis, "Geos and postgis are required.")
+@skipUnlessDBFeature("gis_enabled")
 class GeographyTest(TestCase):
     fixtures = ['initial']
 
@@ -25,6 +25,7 @@ class GeographyTest(TestCase):
         "Ensure geography features loaded properly."
         self.assertEqual(8, City.objects.count())
 
+    @skipUnlessDBFeature("supports_distances_lookups", "supports_distance_geodetic")
     def test02_distance_lookup(self):
         "Testing GeoQuerySet distance lookup support on non-point geography fields."
         z = Zipcode.objects.get(code='77002')
@@ -39,12 +40,14 @@ class GeographyTest(TestCase):
         for cities in [cities1, cities2]:
             self.assertEqual(['Dallas', 'Houston', 'Oklahoma City'], cities)
 
+    @skipUnlessDBFeature("has_distance_method", "supports_distance_geodetic")
     def test03_distance_method(self):
         "Testing GeoQuerySet.distance() support on non-point geography fields."
         # `GeoQuerySet.distance` is not allowed geometry fields.
         htown = City.objects.get(name='Houston')
         Zipcode.objects.distance(htown.point)
 
+    @skipUnless(postgis, "This is a PostGIS-specific test")
     def test04_invalid_operators_functions(self):
         "Ensuring exceptions are raised for operators & functions invalid on geography fields."
         # Only a subset of the geometry functions & operator are available
@@ -89,6 +92,7 @@ class GeographyTest(TestCase):
             self.assertEqual(name, c.name)
             self.assertEqual(state, c.state)
 
+    @skipUnlessDBFeature("has_area_method", "supports_distance_geodetic")
     def test06_geography_area(self):
         "Testing that Area calculations work on geography columns."
         # SELECT ST_Area(poly) FROM geogapp_zipcode WHERE code='77002';
diff --git a/django/contrib/gis/tests/inspectapp/tests.py b/django/contrib/gis/tests/inspectapp/tests.py
index 6a36d3efb7..9dece4372f 100644
--- a/django/contrib/gis/tests/inspectapp/tests.py
+++ b/django/contrib/gis/tests/inspectapp/tests.py
@@ -1,6 +1,7 @@
 from __future__ import unicode_literals
 
 import os
+import re
 from unittest import skipUnless
 
 from django.core.management import call_command
@@ -11,7 +12,7 @@ from django.contrib.gis.geometry.test_data import TEST_DATA
 from django.utils.six import StringIO
 
 if HAS_GDAL:
-    from django.contrib.gis.gdal import Driver
+    from django.contrib.gis.gdal import Driver, OGRException
     from django.contrib.gis.utils.ogrinspect import ogrinspect
 
     from .models import AllOGRFields
@@ -77,23 +78,20 @@ class OGRInspectTest(TestCase):
         self.assertEqual(model_def, '\n'.join(expected))
 
     def test_time_field(self):
-        # Only possible to test this on PostGIS at the moment.  MySQL
-        # complains about permissions, and SpatiaLite/Oracle are
-        # insanely difficult to get support compiled in for in GDAL.
-        if not connections['default'].ops.postgis:
-            self.skipTest("This database does not support 'ogrinspect'ion")
-
         # Getting the database identifier used by OGR, if None returned
         # GDAL does not have the support compiled in.
         ogr_db = get_ogr_db_string()
         if not ogr_db:
-            self.skipTest("Your GDAL installation does not support PostGIS databases")
+            self.skipTest("Unable to setup an OGR connection to your database")
 
-        # Writing shapefiles via GDAL currently does not support writing OGRTime
-        # fields, so we need to actually use a database
-        model_def = ogrinspect(ogr_db, 'Measurement',
-                               layer_key=AllOGRFields._meta.db_table,
-                               decimal=['f_decimal'])
+        try:
+            # Writing shapefiles via GDAL currently does not support writing OGRTime
+            # fields, so we need to actually use a database
+            model_def = ogrinspect(ogr_db, 'Measurement',
+                                   layer_key=AllOGRFields._meta.db_table,
+                                   decimal=['f_decimal'])
+        except OGRException:
+            self.skipTest("Unable to setup an OGR connection to your database")
 
         self.assertTrue(model_def.startswith(
             '# This is an auto-generated Django model module created by ogrinspect.\n'
@@ -111,10 +109,9 @@ class OGRInspectTest(TestCase):
         self.assertIn('    f_char = models.CharField(max_length=10)', model_def)
         self.assertIn('    f_date = models.DateField()', model_def)
 
-        self.assertTrue(model_def.endswith(
-            '    geom = models.PolygonField()\n'
-            '    objects = models.GeoManager()'
-        ))
+        self.assertIsNotNone(re.search(
+            r'    geom = models.PolygonField\(([^\)])*\)\n'  # Some backends may have srid=-1
+            r'    objects = models.GeoManager\(\)', model_def))
 
     def test_management_command(self):
         shp_file = os.path.join(TEST_DATA, 'cities', 'cities.shp')
@@ -142,7 +139,7 @@ def get_ogr_db_string():
         'django.contrib.gis.db.backends.spatialite': ('SQLite', '%(db_name)s', '')
     }
 
-    drv_name, db_str, param_sep = drivers[db['ENGINE']]
+    drv_name, db_str, param_sep = drivers.get(db['ENGINE'])
 
     # Ensure that GDAL library has driver support for the database.
     try:
diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py
index 1e870c678a..5a0d15247e 100644
--- a/django/contrib/gis/tests/layermap/tests.py
+++ b/django/contrib/gis/tests/layermap/tests.py
@@ -8,8 +8,7 @@ import unittest
 from unittest import skipUnless
 
 from django.contrib.gis.gdal import HAS_GDAL
-from django.contrib.gis.tests.utils import mysql
-from django.db import router
+from django.db import connection, router
 from django.conf import settings
 from django.test import TestCase, skipUnlessDBFeature
 from django.utils._os import upath
@@ -151,7 +150,7 @@ class LayerMapTest(TestCase):
             # Unique may take tuple or string parameters.
             for arg in ('name', ('name', 'mpoly')):
                 lm = LayerMapping(County, co_shp, co_mapping, transform=False, unique=arg)
-        except:
+        except Exception:
             self.fail('No exception should be raised for proper use of keywords.')
 
         # Testing invalid params for the `unique` keyword.
@@ -159,7 +158,7 @@ class LayerMapTest(TestCase):
             self.assertRaises(e, LayerMapping, County, co_shp, co_mapping, transform=False, unique=arg)
 
         # No source reference system defined in the shapefile, should raise an error.
-        if not mysql:
+        if connection.features.supports_transform:
             self.assertRaises(LayerMapError, LayerMapping, County, co_shp, co_mapping)
 
         # Passing in invalid ForeignKey mapping parameters -- must be a dictionary
diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py
index 6b7de4a87a..67dd07b583 100644
--- a/django/contrib/gis/tests/relatedapp/tests.py
+++ b/django/contrib/gis/tests/relatedapp/tests.py
@@ -1,7 +1,8 @@
 from __future__ import unicode_literals
 
 from django.contrib.gis.geos import HAS_GEOS
-from django.contrib.gis.tests.utils import mysql, no_oracle
+from django.contrib.gis.tests.utils import no_oracle
+from django.db import connection
 from django.test import TestCase, skipUnlessDBFeature
 
 if HAS_GEOS:
@@ -146,7 +147,7 @@ class RelatedGeoModelTest(TestCase):
         self.assertEqual(1, len(qs))
         self.assertEqual('P2', qs[0].name)
 
-        if not mysql:
+        if connection.features.supports_transform:
             # This time center2 is in a different coordinate system and needs
             # to be wrapped in transformation SQL.
             qs = Parcel.objects.filter(center2__within=F('border1'))
@@ -159,7 +160,7 @@ class RelatedGeoModelTest(TestCase):
         self.assertEqual(1, len(qs))
         self.assertEqual('P1', qs[0].name)
 
-        if not mysql:
+        if connection.features.supports_transform:
             # This time the city column should be wrapped in transformation SQL.
             qs = Parcel.objects.filter(border2__contains=F('city__location__point'))
             self.assertEqual(1, len(qs))
diff --git a/django/contrib/gis/tests/utils.py b/django/contrib/gis/tests/utils.py
index 52c1a6c74e..3a43f8852b 100644
--- a/django/contrib/gis/tests/utils.py
+++ b/django/contrib/gis/tests/utils.py
@@ -28,6 +28,9 @@ postgis = _default_db == 'postgis'
 mysql = _default_db == 'mysql'
 spatialite = _default_db == 'spatialite'
 
+# MySQL spatial indices can't handle NULL geometries.
+gisfield_may_be_null = not mysql
+
 if oracle and 'gis' in settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE']:
     from django.contrib.gis.db.backends.oracle.models import OracleSpatialRefSys as SpatialRefSys
 elif postgis:
diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py
index ead2f170ac..b3765ef8da 100644
--- a/django/contrib/gis/utils/layermapping.py
+++ b/django/contrib/gis/utils/layermapping.py
@@ -103,10 +103,10 @@ class LayerMapping(object):
 
         # Getting the geometry column associated with the model (an
         # exception will be raised if there is no geometry column).
-        if self.spatial_backend.mysql:
-            transform = False
-        else:
+        if connections[self.using].features.supports_transform:
             self.geo_field = self.geometry_field()
+        else:
+            transform = False
 
         # Checking the source spatial reference system, and getting
         # the coordinate transformation object (unless the `transform`