Repository: pandas-dev/pandas

Issue:
TST: Run tests using a database explicitly
At the moment, our tests accessing a database (in `pandas/tests/io/test_sql.py`) are being skipped if the module to connect to the database are not installed (e.g. `pymysql` or `psycopg2`), or if the connection to the database doesn't succeed.

This has two implications:
- It's not easy in the CI to know which builds are running the db tests (we need to check for the dependencies, and also check if a db server is set up in the host)
- If there is an issue with the db, the tests will be skipped silently (we'll log that they are skipped, but I guess nobody will notice)

I think a better approach would be:
- Mark those tests in the cateogry `db` (i.e. `@pytest.mark.db`)
- Remove the skips for when the the connection fails (and possibly the one for when the db modules are not installed)
- In the CI add `-m "not db"` or `-m "db"` depending on whether we want to run the tests or not (this will be done with the env `PATTERNS` when #23924 is merged)

@jreback is this ok?


Discussion:
IMO, this applies verbatim to the tests for boto as well - it's hard to see where it's tested (actually only one build is testing them), and in case of errors, these get silently skipped in the `s3_resource` (except two tests that aren't using the fixture yet, which will be done as a side benefit of #23731).

This is especially critical since the boto tests will *all* be skipped once the environment starts pulling in a newer boto, due to #23754 (that's the reason I've held off on incorporating the feedback for #23731 so far).
I'd like to take this up if the approach is confirmed.
@jreback are you ok with marking the db tests in pytest, and run them explicitly instead of skipping them when the connection fails (which will make them fail silently if the db fails)?
sure as long as they r skipped for users running pd.test() (iow treated like a slow test)

Resolving patch:
diff --git a/pandas/conftest.py b/pandas/conftest.py
--- a/pandas/conftest.py
+++ b/pandas/conftest.py
@@ -34,6 +34,8 @@ def pytest_addoption(parser):
                      help="skip slow tests")
     parser.addoption("--skip-network", action="store_true",
                      help="skip network tests")
+    parser.addoption("--skip-db", action="store_true",
+                     help="skip db tests")
     parser.addoption("--run-high-memory", action="store_true",
                      help="run high memory tests")
     parser.addoption("--only-slow", action="store_true",
@@ -52,6 +54,9 @@ def pytest_runtest_setup(item):
     if 'network' in item.keywords and item.config.getoption("--skip-network"):
         pytest.skip("skipping due to --skip-network")
 
+    if 'db' in item.keywords and item.config.getoption("--skip-db"):
+        pytest.skip("skipping due to --skip-db")
+
     if 'high_memory' in item.keywords and not item.config.getoption(
             "--run-high-memory"):
         pytest.skip(
diff --git a/pandas/util/_tester.py b/pandas/util/_tester.py
--- a/pandas/util/_tester.py
+++ b/pandas/util/_tester.py
@@ -16,7 +16,7 @@ def test(extra_args=None):
         import hypothesis  # noqa
     except ImportError:
         raise ImportError("Need hypothesis>=3.58 to run tests")
-    cmd = ['--skip-slow', '--skip-network']
+    cmd = ['--skip-slow', '--skip-network', '--skip-db']
     if extra_args:
         if not isinstance(extra_args, list):
             extra_args = [extra_args]


Test patch:
diff --git a/pandas/tests/io/test_sql.py b/pandas/tests/io/test_sql.py
--- a/pandas/tests/io/test_sql.py
+++ b/pandas/tests/io/test_sql.py
@@ -1152,14 +1152,8 @@ class _TestSQLAlchemy(SQLAlchemyMixIn, PandasSQLTest):
     def setup_class(cls):
         cls.setup_import()
         cls.setup_driver()
-
-        # test connection
-        try:
-            conn = cls.connect()
-            conn.connect()
-        except sqlalchemy.exc.OperationalError:
-            msg = "{0} - can't connect to {1} server".format(cls, cls.flavor)
-            pytest.skip(msg)
+        conn = cls.connect()
+        conn.connect()
 
     def load_test_data_and_sql(self):
         self._load_raw_sql()
@@ -1791,13 +1785,10 @@ def connect(cls):
 
     @classmethod
     def setup_driver(cls):
-        try:
-            import pymysql  # noqa
-            cls.driver = 'pymysql'
-            from pymysql.constants import CLIENT
-            cls.connect_args = {'client_flag': CLIENT.MULTI_STATEMENTS}
-        except ImportError:
-            pytest.skip('pymysql not installed')
+        pymysql = pytest.importorskip('pymysql')
+        cls.driver = 'pymysql'
+        cls.connect_args = {
+            'client_flag': pymysql.constants.CLIENT.MULTI_STATEMENTS}
 
     def test_default_type_conversion(self):
         df = sql.read_sql_table("types_test_data", self.conn)
@@ -1860,11 +1851,8 @@ def connect(cls):
 
     @classmethod
     def setup_driver(cls):
-        try:
-            import psycopg2  # noqa
-            cls.driver = 'psycopg2'
-        except ImportError:
-            pytest.skip('psycopg2 not installed')
+        pytest.importorskip('psycopg2')
+        cls.driver = 'psycopg2'
 
     def test_schema_support(self):
         # only test this for postgresql (schema's not supported in
@@ -1932,21 +1920,25 @@ def test_schema_support(self):
 
 
 @pytest.mark.single
+@pytest.mark.db
 class TestMySQLAlchemy(_TestMySQLAlchemy, _TestSQLAlchemy):
     pass
 
 
 @pytest.mark.single
+@pytest.mark.db
 class TestMySQLAlchemyConn(_TestMySQLAlchemy, _TestSQLAlchemyConn):
     pass
 
 
 @pytest.mark.single
+@pytest.mark.db
 class TestPostgreSQLAlchemy(_TestPostgreSQLAlchemy, _TestSQLAlchemy):
     pass
 
 
 @pytest.mark.single
+@pytest.mark.db
 class TestPostgreSQLAlchemyConn(_TestPostgreSQLAlchemy, _TestSQLAlchemyConn):
     pass
 
@@ -2189,13 +2181,6 @@ def tquery(query, con=None, cur=None):
         return list(res)
 
 
-def _skip_if_no_pymysql():
-    try:
-        import pymysql  # noqa
-    except ImportError:
-        pytest.skip('pymysql not installed, skipping')
-
-
 @pytest.mark.single
 class TestXSQLite(SQLiteMixIn):
 
@@ -2404,76 +2389,56 @@ def clean_up(test_table_to_drop):
 
 
 @pytest.mark.single
+@pytest.mark.db
 @pytest.mark.skip(reason="gh-13611: there is no support for MySQL "
                   "if SQLAlchemy is not installed")
 class TestXMySQL(MySQLMixIn):
 
     @pytest.fixture(autouse=True, scope='class')
     def setup_class(cls):
-        _skip_if_no_pymysql()
-
-        # test connection
-        import pymysql
-        try:
-            # Try Travis defaults.
-            # No real user should allow root access with a blank password.
-            pymysql.connect(host='localhost', user='root', passwd='',
-                            db='pandas_nosetest')
-        except pymysql.Error:
-            pass
-        else:
-            return
+        pymysql = pytest.importorskip('pymysql')
+        pymysql.connect(host='localhost', user='root', passwd='',
+                        db='pandas_nosetest')
         try:
             pymysql.connect(read_default_group='pandas')
         except pymysql.ProgrammingError:
-            pytest.skip(
+            raise RuntimeError(
                 "Create a group of connection parameters under the heading "
                 "[pandas] in your system's mysql default file, "
-                "typically located at ~/.my.cnf or /etc/.my.cnf. ")
+                "typically located at ~/.my.cnf or /etc/.my.cnf.")
         except pymysql.Error:
-            pytest.skip(
+            raise RuntimeError(
                 "Cannot connect to database. "
                 "Create a group of connection parameters under the heading "
                 "[pandas] in your system's mysql default file, "
-                "typically located at ~/.my.cnf or /etc/.my.cnf. ")
+                "typically located at ~/.my.cnf or /etc/.my.cnf.")
 
     @pytest.fixture(autouse=True)
     def setup_method(self, request, datapath):
-        _skip_if_no_pymysql()
-        import pymysql
+        pymysql = pytest.importorskip('pymysql')
+        pymysql.connect(host='localhost', user='root', passwd='',
+                        db='pandas_nosetest')
         try:
-            # Try Travis defaults.
-            # No real user should allow root access with a blank password.
-            self.conn = pymysql.connect(host='localhost', user='root',
-                                        passwd='', db='pandas_nosetest')
-        except pymysql.Error:
-            pass
-        else:
-            return
-        try:
-            self.conn = pymysql.connect(read_default_group='pandas')
+            pymysql.connect(read_default_group='pandas')
         except pymysql.ProgrammingError:
-            pytest.skip(
+            raise RuntimeError(
                 "Create a group of connection parameters under the heading "
                 "[pandas] in your system's mysql default file, "
-                "typically located at ~/.my.cnf or /etc/.my.cnf. ")
+                "typically located at ~/.my.cnf or /etc/.my.cnf.")
         except pymysql.Error:
-            pytest.skip(
+            raise RuntimeError(
                 "Cannot connect to database. "
                 "Create a group of connection parameters under the heading "
                 "[pandas] in your system's mysql default file, "
-                "typically located at ~/.my.cnf or /etc/.my.cnf. ")
+                "typically located at ~/.my.cnf or /etc/.my.cnf.")
 
         self.method = request.function
 
     def test_basic(self):
-        _skip_if_no_pymysql()
         frame = tm.makeTimeDataFrame()
         self._check_roundtrip(frame)
 
     def test_write_row_by_row(self):
-
-        _skip_if_no_pymysql()
         frame = tm.makeTimeDataFrame()
         frame.iloc[0, 0] = np.nan
         drop_sql = "DROP TABLE IF EXISTS test"
@@ -2493,7 +2458,6 @@ def test_write_row_by_row(self):
         tm.assert_frame_equal(result, frame, check_less_precise=True)
 
     def test_chunksize_read_type(self):
-        _skip_if_no_pymysql()
         frame = tm.makeTimeDataFrame()
         frame.index.name = "index"
         drop_sql = "DROP TABLE IF EXISTS test"
@@ -2508,7 +2472,6 @@ def test_chunksize_read_type(self):
         tm.assert_frame_equal(frame[:chunksize], chunk_df)
 
     def test_execute(self):
-        _skip_if_no_pymysql()
         frame = tm.makeTimeDataFrame()
         drop_sql = "DROP TABLE IF EXISTS test"
         create_sql = sql.get_schema(frame, 'test')
@@ -2528,7 +2491,6 @@ def test_execute(self):
         tm.assert_frame_equal(result, frame[:1])
 
     def test_schema(self):
-        _skip_if_no_pymysql()
         frame = tm.makeTimeDataFrame()
         create_sql = sql.get_schema(frame, 'test')
         lines = create_sql.splitlines()
@@ -2548,7 +2510,6 @@ def test_schema(self):
 
     @tm.capture_stdout
     def test_execute_fail(self):
-        _skip_if_no_pymysql()
         drop_sql = "DROP TABLE IF EXISTS test"
         create_sql = """
         CREATE TABLE test
@@ -2570,7 +2531,6 @@ def test_execute_fail(self):
             sql.execute('INSERT INTO test VALUES("foo", "bar", 7)', self.conn)
 
     def test_execute_closed_connection(self, request, datapath):
-        _skip_if_no_pymysql()
         drop_sql = "DROP TABLE IF EXISTS test"
         create_sql = """
         CREATE TABLE test
@@ -2595,11 +2555,9 @@ def test_execute_closed_connection(self, request, datapath):
         self.setup_method(request, datapath)
 
     def test_na_roundtrip(self):
-        _skip_if_no_pymysql()
         pass
 
     def _check_roundtrip(self, frame):
-        _skip_if_no_pymysql()
         drop_sql = "DROP TABLE IF EXISTS test_table"
         cur = self.conn.cursor()
         with warnings.catch_warnings():
@@ -2636,13 +2594,11 @@ def _check_roundtrip(self, frame):
         tm.assert_frame_equal(expected, result)
 
     def test_keyword_as_column_names(self):
-        _skip_if_no_pymysql()
         df = DataFrame({'From': np.ones(5)})
         sql.to_sql(df, con=self.conn, name='testkeywords',
                    if_exists='replace', index=False)
 
     def test_if_exists(self):
-        _skip_if_no_pymysql()
         df_if_exists_1 = DataFrame({'col1': [1, 2], 'col2': ['A', 'B']})
         df_if_exists_2 = DataFrame(
             {'col1': [3, 4, 5], 'col2': ['C', 'D', 'E']})