Repository: qiskit-community/qiskit-ibm-experiment (language: python)

Issue:
`qiskit.test` is going to be removed in `qiskit 1.0`
Qiskit exposes test utilities as part of its deliverable package. This is not consider good practice and [its removal is a long overdue issue](https://github.com/Qiskit/qiskit/issues/6862). Qiskit teams's intention is [to clean that up for `qiskit 1.0`](https://github.com/Qiskit/qiskit/pull/10998) (planned to be released on Jan 31st, 2024).  The module will be available in the `0.*` versions (EoL, mid 2024) and [raise a deprecation warning](https://github.com/Qiskit/qiskit/pull/11001).

[qiskit-ibm-experiment](https://github.com/Qiskit-Extensions/qiskit-ibm-experiment) uses `qiskit.test` in, at least, the following file:
https://github.com/Qiskit-Extensions/qiskit-ibm-experiment/blob/main/test/service/ibm_test_case.py#L19

Consider stop importing it from qiskit and move the relevant testing-supporting code to `qiskit-ibm-experiment` codebase. 


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/cron-staging.yml b/.github/workflows/cron-staging.yml
index 71338183..58ecf1de 100644
--- a/.github/workflows/cron-staging.yml
+++ b/.github/workflows/cron-staging.yml
@@ -13,6 +13,7 @@ jobs:
       QISKIT_IBM_STAGING_HGP: ${{ secrets.QISKIT_IBM_STAGING_HGP }}
       QISKIT_IBM_STAGING_BACKEND: ${{ secrets.QISKIT_IBM_STAGING_BACKEND }}
       QISKIT_IBM_USE_STAGING_CREDENTIALS: True
+      TEST_TIMEOUT: "0"
     steps:
       - uses: actions/checkout@v2
       - name: Set up Python 3.8
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 9637d9aa..6879bf09 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,17 +1,24 @@
-pylint==2.12.1
+# Test tooling
+fixtures
 stestr
+testtools
+
+# Extra test dependencies
 matplotlib>=2.1
-scipy>=1.0
-black~=22.0
 qiskit-ibm-runtime
 qiskit-experiments
+scipy>=1.0
+
+# Linters
+black~=22.0
+pylint==2.12.1
 
 # Documentation
-sphinx
 jupyter-sphinx
+nbsphinx
+qiskit-sphinx-theme~=1.16.0
+reno>=2.11.0
+sphinx
 sphinx-intl
 sphinx-tabs
 sphinx-autodoc-typehints
-nbsphinx
-reno>=2.11.0
-qiskit-sphinx-theme~=1.16.0
diff --git a/tox.ini b/tox.ini
index 63e1f97a..06d6bd50 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,7 +6,10 @@ skipsdist = True
 [testenv]
 usedevelop = true
 install_command = pip install -U {opts} {packages}
-passenv = QISKIT_IBM_*
+passenv =
+  QISKIT_IBM_*
+  TEST_TIMEOUT
+  QE_USE_TESTTOOLS
 setenv =
   VIRTUAL_ENV={envdir}
   LANGUAGE=en_7US


Test patch:
diff --git a/test/service/ibm_test_case.py b/test/service/ibm_test_case.py
index 9a77be16..9393b392 100644
--- a/test/service/ibm_test_case.py
+++ b/test/service/ibm_test_case.py
@@ -12,53 +12,138 @@
 
 """Custom TestCase for IBM Provider."""
 
-import os
-import logging
 import inspect
+import logging
+import os
+import unittest
+import warnings
+
 from test.utils.utils import setup_test_logging
-from qiskit.test.base import BaseQiskitTestCase
+
+import fixtures
+import testtools
+
 from qiskit_ibm_experiment import QISKIT_IBM_EXPERIMENT_LOGGER_NAME
 
 
-class IBMTestCase(BaseQiskitTestCase):
-    """Custom TestCase for use with the IBM Provider."""
-
-    @classmethod
-    def setUpClass(cls):
-        super().setUpClass()
-        cls.log = logging.getLogger(cls.__name__)
-        filename = f"{os.path.splitext(inspect.getfile(cls))[0]}s.log"
-        setup_test_logging(cls.log, filename)
-        cls._set_logging_level(logging.getLogger(QISKIT_IBM_EXPERIMENT_LOGGER_NAME))
-
-    @classmethod
-    def simple_job_callback(cls, job_id, job_status, job, **kwargs):
-        """A callback function that logs current job status."""
-        # pylint: disable=unused-argument
-        queue_info = kwargs.get("queue_info", "unknown")
-        cls.log.info(
-            "Job %s status is %s, queue_info is %s", job_id, job_status, queue_info
-        )
-
-    @classmethod
-    def _set_logging_level(cls, logger: logging.Logger) -> None:
-        """Set logging level for the input logger.
-
-        Args:
-            logger: Logger whose level is to be set.
-        """
-        if logger.level is logging.NOTSET:
-            try:
-                logger.setLevel(cls.log.level)
-            except Exception as ex:  # pylint: disable=broad-except
-                logger.warning(
-                    'Error while trying to set the level for the "%s" logger to %s. %s.',
-                    logger,
-                    os.getenv("LOG_LEVEL"),
-                    str(ex),
-                )
-        if not any(
-            isinstance(handler, logging.StreamHandler) for handler in logger.handlers
-        ):
-            logger.addHandler(logging.StreamHandler())
-            logger.propagate = False
+# Note: this code is largely based on how qiskit-experiments sets up its
+# TestCase subclass (which was based on how Qiskit set its up).
+
+# Fail tests that take longer than this
+TEST_TIMEOUT = int(os.environ.get("TEST_TIMEOUT", 60))
+# Use testtools by default as a (mostly) drop in replacement for
+# unittest's TestCase. This will enable the fixtures used for capturing stdout
+# stderr, and pylogging to attach the output to stestr's result stream.
+USE_TESTTOOLS = os.environ.get("QE_USE_TESTTOOLS", "TRUE").lower() not in (
+    "false",
+    "0",
+    "no",
+)
+
+
+def create_base_test_case(use_testtools: bool) -> unittest.TestCase:
+    """Create the base test case class for package tests
+
+    This function produces the base class for qiskit-experiments tests using
+    either ``unittest.TestCase`` or ``testtools.TestCase`` for the base class.
+    The creation of the class is done in this function rather than directly
+    executed in the module so that, even when ``USE_TESTTOOLS`` is true, a
+    ``unittest`` base class can be produced for ``test_base.py`` to check that
+    no hard-dependence on ``testtools`` has been introduced.
+    """
+    if use_testtools:
+
+        class BaseTestCase(testtools.TestCase):
+            """Base test class."""
+
+            # testtools maintains their own version of assert functions which mostly
+            # behave as value adds to the std unittest assertion methods. However,
+            # for assertEquals and assertRaises modern unittest has diverged from
+            # the forks in testtools and offer more (or different) options that are
+            # incompatible testtools versions. Just use the stdlib versions so that
+            # our tests work as expected.
+            assertRaises = unittest.TestCase.assertRaises
+            assertEqual = unittest.TestCase.assertEqual
+
+            def setUp(self):
+                super().setUp()
+                if os.environ.get("QISKIT_TEST_CAPTURE_STREAMS"):
+                    stdout = self.useFixture(fixtures.StringStream("stdout")).stream
+                    self.useFixture(fixtures.MonkeyPatch("sys.stdout", stdout))
+                    stderr = self.useFixture(fixtures.StringStream("stderr")).stream
+                    self.useFixture(fixtures.MonkeyPatch("sys.stderr", stderr))
+                    self.useFixture(
+                        fixtures.LoggerFixture(nuke_handlers=False, level=None)
+                    )
+
+    else:
+
+        class BaseTestCase(unittest.TestCase):
+            """Base test class."""
+
+            def useFixture(self, fixture):  # pylint: disable=invalid-name
+                """Shim so that useFixture can be called in subclasses
+
+                useFixture is a testtools.TestCase method. The actual fixture is
+                not used when using unittest.
+                """
+
+    class QIETestCase(BaseTestCase):
+        """qiskit-ibm-experiment specific extra functionality for test cases."""
+
+        def setUp(self):
+            super().setUp()
+            self.useFixture(fixtures.Timeout(TEST_TIMEOUT, gentle=True))
+
+        @classmethod
+        def setUpClass(cls):
+            """Set-up test class."""
+            super().setUpClass()
+            cls.log = logging.getLogger(cls.__name__)
+            filename = f"{os.path.splitext(inspect.getfile(cls))[0]}s.log"
+            setup_test_logging(cls.log, filename)
+            cls._set_logging_level(logging.getLogger(QISKIT_IBM_EXPERIMENT_LOGGER_NAME))
+
+            warnings.filterwarnings("error", category=DeprecationWarning)
+            # Tests should not generate any warnings unless testing those
+            # warnings. In that case, the test should catch the warning
+            # assertWarns or warnings.catch_warnings.
+            warnings.filterwarnings("error", module="qiskit_ibm_experiment")
+
+        @classmethod
+        def simple_job_callback(cls, job_id, job_status, job, **kwargs):
+            """A callback function that logs current job status."""
+            # pylint: disable=unused-argument
+            queue_info = kwargs.get("queue_info", "unknown")
+            cls.log.info(
+                "Job %s status is %s, queue_info is %s", job_id, job_status, queue_info
+            )
+
+        @classmethod
+        def _set_logging_level(cls, logger: logging.Logger) -> None:
+            """Set logging level for the input logger.
+
+            Args:
+                logger: Logger whose level is to be set.
+            """
+            if logger.level is logging.NOTSET:
+                try:
+                    logger.setLevel(cls.log.level)
+                except Exception as ex:  # pylint: disable=broad-except
+                    logger.warning(
+                        'Error while trying to set the level for the "%s" logger to %s. %s.',
+                        logger,
+                        os.getenv("LOG_LEVEL"),
+                        str(ex),
+                    )
+            if not any(
+                isinstance(handler, logging.StreamHandler)
+                for handler in logger.handlers
+            ):
+                logger.addHandler(logging.StreamHandler())
+                logger.propagate = False
+
+    return QIETestCase
+
+
+IBMTestCase = create_base_test_case(USE_TESTTOOLS)
diff --git a/test/service/test_experiment_data_integration.py b/test/service/test_experiment_data_integration.py
index 7a9ac348..4fe5bc47 100644
--- a/test/service/test_experiment_data_integration.py
+++ b/test/service/test_experiment_data_integration.py
@@ -18,9 +18,8 @@
 import contextlib
 from test.service.ibm_test_case import IBMTestCase
 import numpy as np
-from qiskit import transpile, QuantumCircuit
+from qiskit import transpile, QuantumCircuit, QuantumRegister, ClassicalRegister
 from qiskit.providers import JobStatus
-from qiskit.test.reference_circuits import ReferenceCircuits
 from qiskit_experiments.framework import (
     ExperimentData,
     ExperimentDecoder,
@@ -36,6 +35,18 @@
 from qiskit_ibm_experiment.exceptions import IBMApiError
 
 
+def bell():
+    """Return a Bell circuit."""
+    qr = QuantumRegister(2, name="qr")
+    cr = ClassicalRegister(2, name="qc")
+    qc = QuantumCircuit(qr, cr, name="bell")
+    qc.h(qr[0])
+    qc.cx(qr[0], qr[1])
+    qc.measure(qr, cr)
+
+    return qc
+
+
 @skipIf(
     not os.environ.get("QISKIT_IBM_USE_STAGING_CREDENTIALS", ""), "Only runs on staging"
 )
@@ -49,7 +60,7 @@ def setUpClass(cls):
         try:
             cls._setup_service()
             cls._setup_provider()
-            cls.circuit = transpile(ReferenceCircuits.bell(), cls.backend)
+            cls.circuit = transpile(bell(), cls.backend)
         except Exception as err:
             cls.log.info("Error while setting the service/provider: %s", err)
             raise
@@ -113,7 +124,7 @@ def test_add_data_job(self):
             experiment_type="qiskit_test",
             service=self.service,
         )
-        transpiled = transpile(ReferenceCircuits.bell(), self.backend)
+        transpiled = transpile(bell(), self.backend)
         transpiled.metadata = {"foo": "bar"}
         job = self._run_circuit(transpiled)
         exp_data.add_jobs(job)