Repository: pyjanitor-devs/pyjanitor

Issue:
[DOC] Follow PEP 257 Docstring conventions
# Brief Description of Fix

<!-- Please describe the fix in terms of a "before" and "after". In other words, what's not so good about the current docs
page, and what you would like to see it become.

Example starter wording is provided. -->

I propose that we follow the docstring conventions laid out in [PEP 257](https://www.python.org/dev/peps/pep-0257/).

Namely:
>One-liners are for really obvious cases. They should really fit on one line. For example:
```python
def kos_root():
    """Return the pathname of the KOS root directory."""
    global _kos_root
    if _kos_root: return _kos_root
    ...
```
and:
>Multi-line docstrings consist of a summary line just like a one-line docstring, followed by a blank line, followed by a more elaborate description.
```python
def complex(real=0.0, imag=0.0):
    """Form a complex number.

    Keyword arguments:
    real -- the real part (default 0.0)
    imag -- the imaginary part (default 0.0)
    """
    if imag == 0.0 and real == 0.0:
        return complex_zero
    ...
```

We're using Sphinx, so the docstring will have a different body, but we can still follow the formatting.

Following `pydocstyle`, the first line of the docstring should end in a period and be in imperative mood:
```python
D401: First line should be in imperative mood: 'Do', not 'Does'.

[Docstring] prescribes the function or method's effect as a command:
("Do this", "Return that"), not as a description; e.g. don't write "Returns the pathname ...".
```

Additionally, all tests should have a one line docstring that explains what the test does (for everyone's sake), and what it is testing for. We don't actually have this in the docs, but it is part of my ongoing campaign with #306 

The PR for this issue (which I am happy to do), will involve updating the docs to reflect these standards, and updating docstrings throughout the project.


# Relevant Context

<!-- Please put here, in bullet points, links to the relevant docs page. A few starting template points are available
to get you started. -->

- [Link to documentation page](https://ericmjl.github.io/pyjanitor/CONTRIBUTION_TYPES.html)
- [Link to exact file to be edited](https://github.com/ericmjl/pyjanitor/blob/dev/docs/CONTRIBUTION_TYPES.rst)



Discussion:
We have some variety in docstrings in `pyjanitor`, perhaps in part because this aspect isn't spelled out, or people have their own personal preferences or habits, so I welcome any discussion on this.
@hectormz this would be great. I vote in favour; how about others on the dev team?
I personally would welcome  a guideline on documentation, my only question is the status of examples inside the docstring? 
And maybe also the potential relationship between the test cases, the examples inside the docstring and the notebook examples.
@VPerrollaz could you clarify what you mean please? Just want to make sure your thoughts are spelled out clearly for others. Do you mean that they should be harmonized, i.e. the examples in the docstring should also be test cases?
My point was that in many cases the tests actually make good examples so should we encourage some kind of harmonization?
I think it is not possible to be too systematic because in some test case the setup of the dataframe is too complicated to be included.
@VPerrollaz I see what you mean. Depending on the function/test, the test can make a good example. In our documentation, we could encourage that type of duplication for ease/if the author thinks it makes sense. 
In this issue, I was just focusing essentially on what the first line of the docstrings should be like (and that there be an empty line after). I've also been kicking some thoughts about our docs in general, so once I start that issue, there may be relevant points to be discussed there
Great discussion, sirs!

I'd like to advocate for atomic/simple changes. Even systematic ones, like those that @hectormz has done before, were done in batches (PR-by-PR). I have prior experience with that, when I helped redo about 70% of the pylab examples in matplotlib, so I don't think there's an issue with doing it this way. (Clean commit histories are generally quite nice, but I won't bend over backwards to make that a thing.)

Let's focus here on getting the docstring first line done right. @VPerrollaz, if you are feeling up for it, could you make an issue for harmonizing the examples, and propose what you think could be the right way forward? Having seen your work, I think my priors are that you'll make a well-structured suggestion that we can act on.
- Ok I thought @hectormz was starting a general docstring convention, sorry. 
- One of the reasons I mentioned it was that I say a few other issues related to examples, notebook and so on I will try to have a look and see if there is some underlying central issue in those cases.
I'll continue with this
I believe https://github.com/ericmjl/pyjanitor/blob/dev/janitor/spark/backend.py is using `Numpy` style docstrings. Should I convert them to `reStructuredText` style in the process?

Resolving patch:
diff --git a/janitor/finance.py b/janitor/finance.py
--- a/janitor/finance.py
+++ b/janitor/finance.py
@@ -13,7 +13,7 @@
 from janitor import check
 from janitor.errors import JanitorError
 
-from .utils import deprecated_alias
+from .utils import deprecated_alias, is_connected
 
 currency_set = {
     "AUD",
@@ -694,3 +694,53 @@ def inflate_currency(
         df[column_name] = df[column_name] * inflator
 
     return df
+
+
+def convert_stock(stock_symbol: str) -> str:
+    """
+    This function takes in a stock symbol as a parameter,
+    queries an API for the companies full name and returns
+    it
+
+    Example:
+        print(convert_stock("aapl"))
+
+        console >> Apple Inc.
+
+    :param stock_symbol: This is our input stock symbol
+        to be converted
+    :raises ConnectionError: if stock ticker data cannot be retrieved
+    :return: We return the full company name
+    """
+    if is_connected("www.google.com"):
+        stock_symbol = stock_symbol.upper()
+        return get_symbol(stock_symbol.upper())
+    else:
+        raise ConnectionError(
+            "Connection Error: Client Not Connected to Internet"
+        )
+
+
+def get_symbol(symbol: str):
+    """
+    This is a helper function to get a companies full
+    name based on the stock symbol.
+
+    Example:
+        print(get_symbol("aapl"))
+        console >> Apple Inc.
+
+    :param symbol: This is our stock symbol that we use
+        to query te api for the companies full name.
+    :return: This is the company name
+    """
+    result = requests.get(
+        "http://d.yimg.com/autoc."
+        + "finance.yahoo.com/autoc?query={}&region=1&lang=en".format(symbol)
+    ).json()
+
+    for x in result["ResultSet"]["Result"]:
+        if x["symbol"] == symbol:
+            return x["name"]
+        else:
+            return None
diff --git a/janitor/functions.py b/janitor/functions.py
--- a/janitor/functions.py
+++ b/janitor/functions.py
@@ -27,17 +27,17 @@
 import pandas_flavor as pf
 from multipledispatch import dispatch
 from natsort import index_natsorted
-from pandas.api.types import union_categoricals, is_list_like, is_bool_dtype
+from pandas.api.types import is_bool_dtype, is_list_like, union_categoricals
 from pandas.errors import OutOfBoundsDatetime
 from scipy.stats import mode
 from sklearn.preprocessing import LabelEncoder
 
 from .errors import JanitorError
 from .utils import (
-    _computations_expand_grid,
     _clean_accounting_column,
     _computations_as_categorical,
     _computations_complete,
+    _computations_expand_grid,
     _computations_pivot_longer,
     _computations_pivot_wider,
     _currency_column_to_numeric,
@@ -320,6 +320,7 @@ def clean_names(
         df = df.rename(columns=_strip_accents)
 
     df = df.rename(columns=lambda x: re.sub("_+", "_", x))  # noqa: PD005
+
     df = _strip_underscores(df, strip_underscores)
 
     df = df.rename(columns=lambda x: x[:truncate_limit])
@@ -332,6 +333,7 @@ def clean_names(
 
 def _change_case(col: str, case_type: str) -> str:
     """Change case of a column name."""
+
     case_types = ["preserve", "upper", "lower", "snake"]
     if case_type.lower() not in case_types:
         raise JanitorError(f"case_type must be one of: {case_types}")
@@ -349,6 +351,7 @@ def _change_case(col: str, case_type: str) -> str:
 
 def _remove_special(col_name: Hashable) -> str:
     """Remove special characters from column name."""
+
     return "".join(
         item for item in str(col_name) if item.isalnum() or "_" in item
     )
@@ -364,16 +367,16 @@ def _camel2snake(col_name: str) -> str:
     Implementation taken from: https://gist.github.com/jaytaylor/3660565
     by @jtaylor
     """
-
     subbed = _underscorer1.sub(r"\1_\2", col_name)  # noqa: PD005
     return _underscorer2.sub(r"\1_\2", subbed).lower()  # noqa: PD005
 
 
-FIXES = [(r"[ /:,?()\.-]", "_"), (r"['’]", "")]
+FIXES = [(r"[ /:,?()\.-]", "_"), (r"['’]", ""), (r"[\xa0]", "_")]
 
 
 def _normalize_1(col_name: Hashable) -> str:
     """Perform normalization of column name."""
+
     result = str(col_name)
     for search, replace in FIXES:
         result = re.sub(search, replace, result)  # noqa: PD005
@@ -3760,6 +3763,85 @@ def to_datetime(
     return df
 
 
+@pf.register_dataframe_method
+def truncate_datetime_dataframe(
+    df: pd.DataFrame, datepart: str
+) -> pd.DataFrame:
+    for i in df.columns:
+        for j in df.index:
+            try:
+                df[i][j] = truncate_datetime(datepart, df[i][j])
+            except KeyError:
+                pass
+            except TypeError:
+                pass
+            except AttributeError:
+                pass
+
+    return df
+
+
+@pf.register_dataframe_method
+def truncate_datetime(datepart: str, timestamp: dt.datetime):
+    """
+    Truncate times down to a user-specified precision of
+    year, month, day, hour, minute, or second.
+
+    Call on datetime object to truncate it.
+    Calling on existing df will not alter the contents
+    of said df.
+
+    Note: Truncating down to a Month or Day will yields 0s,
+    as there is no 0 month or 0 day in most datetime systems.
+
+    :param datepart: Truncation precision, YEAR, MONTH, DAY,
+        HOUR, MINUTE, SECOND. (String is automagically
+        capitalized)
+    :param timestamp: expecting a datetime from python datetime class (dt)
+    :raises KeyError: if inappropriate precision is passed
+
+    :returns: a truncated datetime object to
+        the precision specified by datepart.
+    """
+    recurrence = [0, 1, 1, 0, 0, 0]  # [YEAR, MONTH, DAY, HOUR, MINUTE, SECOND]
+    datepart = datepart.upper()
+    ENUM = {
+        "YEAR": 0,
+        "MONTH": 1,
+        "DAY": 2,
+        "HOUR": 3,
+        "MINUTE:": 4,
+        "SECOND": 5,
+        0: timestamp.year,
+        1: timestamp.month,
+        2: timestamp.day,
+        3: timestamp.hour,
+        4: timestamp.minute,
+        5: timestamp.second,
+    }
+    try:
+        ENUM[datepart]
+    # Capture the error but replace it with explicit instructions.
+    except KeyError:
+        msg = (
+            "Invalid truncation. Please enter any one of 'year', "
+            "'month', 'day', 'hour', 'minute' or 'second'."
+        )
+        raise KeyError(msg)
+
+    for i in range(ENUM.get(datepart) + 1):
+        recurrence[i] = ENUM.get(i)
+
+    return dt.datetime(
+        recurrence[0],
+        recurrence[1],
+        recurrence[2],
+        recurrence[3],
+        recurrence[4],
+        recurrence[5],
+    )
+
+
 @pf.register_dataframe_method
 @deprecated_alias(new_column="new_column_name", agg_column="agg_column_name")
 def groupby_agg(
@@ -4537,6 +4619,68 @@ def sort_naturally(
     return df.iloc[new_order, :]
 
 
+def sort_column_value_order(
+    df: pd.DataFrame, column: str, column_value_order: dict, columns=None
+) -> pd.DataFrame:
+    """
+    This function adds precedence to certain values in a specified column, then
+    sorts based on that column and any other specified columns.
+
+    Example:
+                    SalesMonth	Company2	Company3
+        Company1
+        150.0	    Jan	        180.0	    400.0
+        200.0	    Feb	        250.0	    500.0
+        200.0	    Feb	        250.0	    500.0
+        300.0	    Mar	        NaN	        600.0
+        400.0	    April	    500.0	    675.0
+
+        Given the current DataFrame, we want to order the sales month in desc
+        order. To achieve this we would assign the later months with smaller
+        values with the latest month, such as April with the precedence of 0.
+
+        df = sort_column_value_order(
+        df,
+        'SalesMonth',
+        {'April':1,'Mar':2,'Feb':3,'Jan':4}
+        )
+
+        The returned DataFrame will look as follows.
+
+                    SalesMonth	Company2	Company3
+        Company1
+        400.0	    April	    500.0	    675.0
+        300.0	    Mar	        NaN	        600.0
+        200.0	    Feb	        250.0	    500.0
+        200.0	    Feb	        250.0	    500.0
+        150.0	    Jan	        180.0	    400.0
+
+    :param df: This is our DataFrame that we are manipulating
+    :param column: This is a column name as a string we are using to specify
+        which column to sort by
+    :param column_value_order: This is a dictionary of values that will
+        represent precedence of the values in the specified column
+    :param columns: This is a list of additional columns that we can sort by
+    :raises ValueError: raises error if chosen Column Name is not in
+        Dataframe, or if column_value_order dictionary is empty.
+    :return: This function returns a Pandas DataFrame
+    """
+    if len(column_value_order) > 0:
+        if column in df.columns:
+            df["cond_order"] = df[column].replace(column_value_order)
+            if columns is None:
+                new_df = df.sort_values("cond_order")
+                del new_df["cond_order"]
+            else:
+                new_df = df.sort_values(columns + ["cond_order"])
+                del new_df["cond_order"]
+            return new_df
+        else:
+            raise ValueError("Column Name not in DataFrame")
+    else:
+        raise ValueError("column_value_order dictionary cannot be empty")
+
+
 @pf.register_dataframe_method
 def expand_grid(
     df: Optional[pd.DataFrame] = None,
@@ -5260,6 +5404,7 @@ def complete(
     Let's get all the missing years per state::
 
         df.complete(
+
             columns = [{'year': new_year_values}],
             by='state'
         )
diff --git a/janitor/utils.py b/janitor/utils.py
--- a/janitor/utils.py
+++ b/janitor/utils.py
@@ -4,8 +4,8 @@
 import functools
 import os
 import re
+import socket
 import sys
-
 import warnings
 from collections.abc import Callable as dispatch_callable
 from itertools import chain, combinations
@@ -25,12 +25,12 @@
 import pandas as pd
 from pandas.api.types import (
     CategoricalDtype,
-    is_scalar,
     is_extension_array_dtype,
     is_list_like,
+    is_scalar,
 )
-
 from pandas.core.common import apply_if_callable
+
 from .errors import JanitorError
 
 
@@ -1887,6 +1887,38 @@ def _computations_as_categorical(df: pd.DataFrame, **kwargs) -> pd.DataFrame:
     return df
 
 
+def is_connected(url: str) -> bool:
+    """
+    This is a helper function to check if the client
+    is connected to the internet.
+
+    Example:
+        print(is_connected("www.google.com"))
+        console >> True
+
+    :param url: We take a test url to check if we are
+        able to create a valid connection.
+    :raises OSError: if connection to ``URL`` cannot be
+        established
+    :return: We return a boolean that signifies our
+        connection to the internet
+    """
+    try:
+        sock = socket.create_connection((url, 80))
+        if sock is not None:
+            sock.close()
+            return True
+    except OSError as e:
+        import warnings
+
+        warnings.warn(
+            "There was an issue connecting to the internet. "
+            "Please see original error below."
+        )
+        raise e
+    return False
+
+
 @functools.singledispatch
 def _encode_categories(cat, df, column):
     """


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -73,9 +73,9 @@ jobs:
           python -m pip install -e .
           pytest
 
-      - name: Inspect coverage
-        run: |
-          cat coverage.xml
+#      - name: Inspect coverage
+#        run: |
+#          cat coverage.xml
 
       - name: Upload code coverage
         run: |
diff --git a/tests/finance/test_convert_stock.py b/tests/finance/test_convert_stock.py
new file mode 100644
--- /dev/null
+++ b/tests/finance/test_convert_stock.py
@@ -0,0 +1,24 @@
+from janitor.finance import get_symbol
+
+"""
+Tests get_symbol function,
+get_symbol should return appropriate string
+corresponding to abbreviation.
+This string will be a company's full name,
+and the abbreviation will be the NSYE
+symbol for the company.
+
+Example:
+    print(get_symbol("aapl"))
+    console >> Apple Inc.
+
+If the symbol does not have a corresponding
+company, Nonetype should be returned.
+
+"""
+
+
+def test_convert_stock():
+    assert get_symbol("GME") == "GameStop Corp."
+    assert get_symbol("AAPL") != "Aramark"
+    assert get_symbol("ASNF") is None
diff --git a/tests/finance/test_get_symbol.py b/tests/finance/test_get_symbol.py
new file mode 100644
--- /dev/null
+++ b/tests/finance/test_get_symbol.py
@@ -0,0 +1,20 @@
+from janitor.finance import get_symbol
+
+"""
+tests the convert_symbol helper function.
+
+Test 1: GME is Gamestop Corp. Test should run fine.
+Test 2: GME is not Globius Medical Inc.
+Test 3: A little redundant, but it's another
+    'happy path' to show get_symbol works for more
+    abbreviations than just the one tested so far.
+Test 4: ZZZZ does not belong to any company,
+    it should therefore it should be None
+"""
+
+
+def test_get_symbol():
+    assert get_symbol("GME") == "GameStop Corp."
+    assert get_symbol("GME") != "Globus Medical Inc."
+    assert get_symbol("F") == "Ford Motor Company"
+    assert get_symbol("ZZZZ") is None
diff --git a/tests/functions/test_clean_names.py b/tests/functions/test_clean_names.py
--- a/tests/functions/test_clean_names.py
+++ b/tests/functions/test_clean_names.py
@@ -163,6 +163,7 @@ def test_clean_names_camelcase_to_snake_multi(dataframe):
     ]
 
 
+@pytest.mark.functions
 def test_clean_names_enforce_string(dataframe):
     df = dataframe.rename(columns={"a": 1}).clean_names(enforce_string=True)
     for c in df.columns:
@@ -174,3 +175,34 @@ def test_clean_names_truncate_limit(dataframe):
     df = dataframe.clean_names(truncate_limit=7)
     expected_columns = ["a", "bell_ch", "decorat", "animals", "cities"]
     assert set(df.columns) == set(expected_columns)
+
+
+"""
+The following tests ensure nonstandard characters
+and spaces have been cleaned up.
+"""
+
+
+@pytest.mark.functions
+def test_charac():
+    table_GDP = pd.read_html(
+        "https://en.wikipedia.org/wiki/Economy_of_the_United_States",
+        match="Nominal GDP",
+    )
+    df = table_GDP[0]
+
+    df = df.clean_names(strip_underscores=True, case_type="lower")
+
+    assert "current_accountbalance_in_%_of_gdp" in df.columns.values
+
+
+@pytest.mark.functions
+def test_space():
+    table_GDP = pd.read_html(
+        "https://en.wikipedia.org/wiki/Economy_of_Russia", match="Year"
+    )
+    df = table_GDP[0]
+
+    df = df.clean_names(strip_underscores=True, case_type="lower")
+
+    assert ("in %" in df.columns.values) is False
diff --git a/tests/functions/test_encode_categorical.py b/tests/functions/test_encode_categorical.py
--- a/tests/functions/test_encode_categorical.py
+++ b/tests/functions/test_encode_categorical.py
@@ -415,6 +415,10 @@ def test_various(df_in, df_out, kwargs):
     """
     Test output for various combinations.
     """
+    # result = df_in.encode_categorical(
+    #     **kwargs,
+    # )
+    # assert_frame_equal(result, df_out)
     result = df_in.encode_categorical(**kwargs,)
     # directly comparing columns is safe -
     # if the columns have differing categories
diff --git a/tests/functions/test_select_columns.py b/tests/functions/test_select_columns.py
--- a/tests/functions/test_select_columns.py
+++ b/tests/functions/test_select_columns.py
@@ -1,5 +1,5 @@
-import pytest
 import pandas as pd
+import pytest
 from pandas.testing import assert_frame_equal
 
 
diff --git a/tests/functions/test_sort_column_value_order.py b/tests/functions/test_sort_column_value_order.py
new file mode 100644
--- /dev/null
+++ b/tests/functions/test_sort_column_value_order.py
@@ -0,0 +1,59 @@
+import numpy as np
+import pandas as pd
+import pytest
+
+from janitor.functions import sort_column_value_order
+
+"""
+Below, company_sales and company_sales_2 are both dfs.
+
+company_sales_2 is inverted, April is the first month
+    where in comapny_sales Jan is the first month
+
+The values found in each row are the same
+    company_sales's Jan row contains the
+    same values as company_sales_2's Jan row
+
+Test 1 asserts that dfcannot be blank
+
+Test 2 asserts that column cannot be blank
+
+Test 3 asserts that company_sales_2 and
+    company_sales with columns sorted
+    will become equivilent, meaning
+    the columns have been successfully ordered.
+"""
+
+
+def test_sort_column_value_order():
+    company_sales = {
+        "SalesMonth": ["Jan", "Feb", "Feb", "Mar", "April"],
+        "Company1": [150.0, 200.0, 200.0, 300.0, 400.0],
+        "Company2": [180.0, 250.0, 250.0, np.nan, 500.0],
+        "Company3": [400.0, 500.0, 500.0, 600.0, 675.0],
+    }
+    df = pd.DataFrame.from_dict(company_sales)
+    df = df.set_index("Company1")
+    company_sales_2 = {
+        "SalesMonth": ["April", "Mar", "Feb", "Feb", "Jan"],
+        "Company1": [400.0, 300.0, 200.0, 200.0, 150.0],
+        "Company2": [500.0, np.nan, 250.0, 250.0, 180.0],
+        "Company3": [675.0, 600.0, 500.0, 500.0, 400.0],
+    }
+    df2 = pd.DataFrame.from_dict(company_sales_2)
+    df2 = df2.set_index("Company1")
+    with pytest.raises(ValueError):
+        assert pd.DataFrame().equals(
+            sort_column_value_order(pd.DataFrame(), "", {})
+        )
+    with pytest.raises(ValueError):
+        assert pd.DataFrame().equals(
+            sort_column_value_order(
+                df, "", {"April": 1, "Mar": 2, "Feb": 3, "Jan": 4}
+            )
+        )
+    assert df2.equals(
+        sort_column_value_order(
+            df, "SalesMonth", {"April": 1, "Mar": 2, "Feb": 3, "Jan": 4}
+        )
+    )
diff --git a/tests/functions/test_truncate_datetime.py b/tests/functions/test_truncate_datetime.py
new file mode 100644
--- /dev/null
+++ b/tests/functions/test_truncate_datetime.py
@@ -0,0 +1,58 @@
+from datetime import datetime
+
+import pytest
+
+from janitor import truncate_datetime
+
+"""
+Creates 2 datetime objects, one
+    will be a valid object and the
+    other will be Nonetype.
+
+Test 1 asserts valid object is truncated
+    correctly, this is trivial on the
+    first day of every month.
+
+Test 2 asserts that the time[] value
+    is accurate to the specified precision,
+    in this case, month.
+
+Test 3 asserts the valid object truncated
+    correctly to the specified precision
+    in this case, month.
+
+Test 4 asserts that if bad data is passed
+    it will return a Nonetype, and
+    appropriate error handling will
+    take care of it.
+
+"""
+
+
+@pytest.mark.functions
+def test_truncate_datetime():
+    x = datetime.now()
+    x = truncate_datetime("month", x)
+
+    time = {
+        "Year": [x.year],
+        "Month": [x.month],
+        "Day": [x.day],
+        "Hour": [x.hour],
+        "Minute": [x.minute],
+        "Second": [x.second],
+    }
+
+    # time[] returns datetime object, needs indexing.
+    assert time["Day"][0] == 1
+    assert time["Month"][0] == datetime.now().month
+    assert time["Month"][0] == x.month
+
+
+# bad data, error handling test
+@pytest.mark.functions
+def test_bad_data():
+    with pytest.raises(KeyError):
+        y = datetime.now()
+        y = truncate_datetime("mon", y)
+        assert y is None
diff --git a/tests/functions/test_truncate_datetime_dataframe.py b/tests/functions/test_truncate_datetime_dataframe.py
new file mode 100644
--- /dev/null
+++ b/tests/functions/test_truncate_datetime_dataframe.py
@@ -0,0 +1,17 @@
+from datetime import datetime
+
+import numpy as np
+import pandas as pd
+
+from janitor import truncate_datetime
+
+
+def test_truncate_datetime_dataframe():
+    x = datetime.now()
+
+    time = {"datetime: ": [x], "Missing Data: ": np.NAN}
+
+    df = pd.DataFrame.from_dict(time)
+    df.truncate_datetime_dataframe("Year")
+
+    assert df["datetime: "][0] == truncate_datetime("year", x)
diff --git a/tests/utils/test_is_connected.py b/tests/utils/test_is_connected.py
new file mode 100644
--- /dev/null
+++ b/tests/utils/test_is_connected.py
@@ -0,0 +1,35 @@
+from janitor.utils import is_connected
+import socket
+import pytest
+
+
+"""
+Tests the is_connected helper function,
+    which is a function to check if the client
+    is connected to the internet.
+
+Example:
+    print(is_connected("www.google.com"))
+    console >> True
+
+Test 1: happy path, ensures function work
+
+Test 2: web addresses that are not recognized
+    will return false (comzzz is not a tld).
+
+Test 3: web addresses that are not recognized
+    will return false (aadsfff.com does not exist
+    at time of testing).
+
+If test 3 fails, perhaps this is because
+    the website now exists. If that is the case,
+    alter or delete the test.
+"""
+
+
+def test_is_connected():
+    assert is_connected("www.google.com")
+    with pytest.raises(socket.gaierror):
+        assert is_connected("www.google.comzzz") is False
+    with pytest.raises(socket.gaierror):
+        assert is_connected("aadsfff.com") is False