diff --git a/django/db/backends/mysql/features.py b/django/db/backends/mysql/features.py
index a34741267a..7b9a90ab06 100644
--- a/django/db/backends/mysql/features.py
+++ b/django/db/backends/mysql/features.py
@@ -305,6 +305,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
         """
         return self._mysql_storage_engine != "MyISAM"
 
+    uses_savepoints = property(operator.attrgetter("supports_transactions"))
+    can_release_savepoints = property(operator.attrgetter("supports_transactions"))
+
     @cached_property
     def ignores_table_name_case(self):
         return self.connection.mysql_server_data["lower_case_table_names"]
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index 72fac695dd..145add114c 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -30,6 +30,7 @@ from django.contrib.contenttypes.models import ContentType
 from django.core import mail
 from django.core.checks import Error
 from django.core.files import temp as tempfile
+from django.db import connection
 from django.forms.utils import ErrorList
 from django.template.response import TemplateResponse
 from django.test import (
@@ -7022,7 +7023,8 @@ class UserAdminTest(TestCase):
         # Don't depend on a warm cache, see #17377.
         ContentType.objects.clear_cache()
 
-        with self.assertNumQueries(10):
+        expected_num_queries = 10 if connection.features.uses_savepoints else 8
+        with self.assertNumQueries(expected_num_queries):
             response = self.client.get(reverse("admin:auth_user_change", args=(u.pk,)))
             self.assertEqual(response.status_code, 200)
 
@@ -7069,7 +7071,8 @@ class GroupAdminTest(TestCase):
         # Ensure no queries are skipped due to cached content type for Group.
         ContentType.objects.clear_cache()
 
-        with self.assertNumQueries(8):
+        expected_num_queries = 8 if connection.features.uses_savepoints else 6
+        with self.assertNumQueries(expected_num_queries):
             response = self.client.get(reverse("admin:auth_group_change", args=(g.pk,)))
             self.assertEqual(response.status_code, 200)
 
diff --git a/tests/backends/mysql/test_features.py b/tests/backends/mysql/test_features.py
index ec5bd442fb..88e267f048 100644
--- a/tests/backends/mysql/test_features.py
+++ b/tests/backends/mysql/test_features.py
@@ -11,6 +11,7 @@ class TestFeatures(TestCase):
         """
         All storage engines except MyISAM support transactions.
         """
+        del connection.features.supports_transactions
         with mock.patch(
             "django.db.connection.features._mysql_storage_engine", "InnoDB"
         ):
diff --git a/tests/select_for_update/tests.py b/tests/select_for_update/tests.py
index 0251014541..5f5ada8939 100644
--- a/tests/select_for_update/tests.py
+++ b/tests/select_for_update/tests.py
@@ -291,7 +291,7 @@ class SelectForUpdateTests(TransactionTestCase):
             qs = Person.objects.select_for_update(of=("self", "born"))
             self.assertIs(qs.exists(), True)
 
-    @skipUnlessDBFeature("has_select_for_update_nowait")
+    @skipUnlessDBFeature("has_select_for_update_nowait", "supports_transactions")
     def test_nowait_raises_error_on_block(self):
         """
         If nowait is specified, we expect an error to be raised rather
@@ -312,7 +312,7 @@ class SelectForUpdateTests(TransactionTestCase):
         self.end_blocking_transaction()
         self.assertIsInstance(status[-1], DatabaseError)
 
-    @skipUnlessDBFeature("has_select_for_update_skip_locked")
+    @skipUnlessDBFeature("has_select_for_update_skip_locked", "supports_transactions")
     def test_skip_locked_skips_locked_rows(self):
         """
         If skip_locked is specified, the locked row is skipped resulting in
@@ -599,7 +599,7 @@ class SelectForUpdateTests(TransactionTestCase):
         p = Person.objects.get(pk=self.person.pk)
         self.assertEqual("Fred", p.name)
 
-    @skipUnlessDBFeature("has_select_for_update")
+    @skipUnlessDBFeature("has_select_for_update", "supports_transactions")
     def test_raw_lock_not_available(self):
         """
         Running a raw query which can't obtain a FOR UPDATE lock raises
diff --git a/tests/transaction_hooks/tests.py b/tests/transaction_hooks/tests.py
index 4a563088ab..75cac5a3e9 100644
--- a/tests/transaction_hooks/tests.py
+++ b/tests/transaction_hooks/tests.py
@@ -8,6 +8,7 @@ class ForcedError(Exception):
     pass
 
 
+@skipUnlessDBFeature("supports_transactions")
 class TestConnectionOnCommit(TransactionTestCase):
     """
     Tests for transaction.on_commit().
diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py
index 342434666e..a528ab22e5 100644
--- a/tests/transactions/tests.py
+++ b/tests/transactions/tests.py
@@ -370,6 +370,7 @@ class AtomicErrorsTests(TransactionTestCase):
         self.assertEqual(Reporter.objects.count(), 0)
 
 
+@skipUnlessDBFeature("uses_savepoints")
 @skipUnless(connection.vendor == "mysql", "MySQL-specific behaviors")
 class AtomicMySQLTests(TransactionTestCase):