# yellowbrick.features.rfecv
# Visualize the number of features selected with recursive feature elimination
#
# Author:  Benjamin Bengfort <benjamin@bengfort.com>
# Created: Tue Apr 03 17:31:37 2018 -0400
#
# ID: rfecv.py [] benjamin@bengfort.com $

"""
Visualize the number of features selected using recursive feature elimination
"""

##########################################################################
## Imports
##########################################################################

import numpy as np

from yellowbrick.base import ModelVisualizer
from yellowbrick.exceptions import YellowbrickValueError

from sklearn.utils import check_X_y
from sklearn.feature_selection import RFE
from sklearn.model_selection import cross_val_score


##########################################################################
## Recursive Feature Elimination
##########################################################################

class RFECV(ModelVisualizer):
    """
    Recursive Feature Elimination, Cross-Validated (RFECV) feature selection.

    Selects the best subset of features for the supplied estimator by removing
    0 to N features (where N is the number of features) using recursive
    feature elimination, then selecting the best subset based on the
    cross-validation score of the model. Recursive feature elimination
    eliminates n features from a model by fitting the model multiple times and
    at each step, removing the weakest features, determined by either the
    ``coef_`` or ``feature_importances_`` attribute of the fitted model.

    The visualization plots the score relative to each subset and shows trends
    in feature elimination. If the feature elimination CV score is flat, then
    potentially there are not enough features in the model. An ideal curve is
    when the score jumps from low to high as the number of features removed
    increases, then slowly decreases again from the optimal number of
    features.

    Parameters
    ----------
    model : a scikit-learn estimator
        An object that implements ``fit`` and provides information about the
        relative importance of features with either a ``coef_`` or
        ``feature_importances_`` attribute.

        Note that the object is cloned for each validation.

    ax : matplotlib.Axes object, optional
        The axes object to plot the figure on.

    step : int or float, optional (default=1)
        If greater than or equal to 1, then step corresponds to the (integer)
        number of features to remove at each iteration. If within (0.0, 1.0),
        then step corresponds to the percentage (rounded down) of features to
        remove at each iteration.

    groups : array-like, with shape (n_samples,), optional
        Group labels for the samples used while splitting the dataset into
        train/test set.

    cv : int, cross-validation generator or an iterable, optional
        Determines the cross-validation splitting strategy.
        Possible inputs for cv are:

          - None, to use the default 3-fold cross-validation,
          - integer, to specify the number of folds.
          - An object to be used as a cross-validation generator.
          - An iterable yielding train/test splits.

        see the scikit-learn
        `cross-validation guide <http://scikit-learn.org/stable/modules/cross_validation.html>`_
        for more information on the possible strategies that can be used here.

    scoring : string, callable or None, optional, default: None
        A string or scorer callable object / function with signature
        ``scorer(estimator, X, y)``. See scikit-learn model evaluation
        documentation for names of possible metrics.

    kwargs : dict
        Keyword arguments that are passed to the base class and may influence
        the visualization as defined in other Visualizers.

    Attributes
    ----------
    n_features_ : int
        The number of features in the selected subset

    support_ : array of shape [n_features]
        A mask of the selected features

    ranking_ : array of shape [n_features]
        The feature ranking, such that ``ranking_[i]`` corresponds to the
        ranked position of feature i. Selected features are assigned rank 1.

    cv_scores_ : array of shape [n_subsets_of_features, n_splits]
        The cross-validation scores for each subset of features and splits in
        the cross-validation strategy.

    rfe_estimator_ : sklearn.feature_selection.RFE
        A fitted RFE estimator wrapping the original estimator. All estimator
        functions such as ``predict()`` and ``score()`` are passed through to
        this estimator (it rewraps the original model).

    n_feature_subsets_ : array of shape [n_subsets_of_features]
        The number of features removed on each iteration of RFE, computed by the
        number of features in the dataset and the step parameter.

    Notes
    -----
    This model wraps ``sklearn.feature_selection.RFE`` and not
    ``sklearn.feature_selection.RFECV`` because access to the internals of the
    CV and RFE estimators is required for the visualization. The visualizer
    does take similar arguments, however it does not expose the same internal
    attributes.

    Additionally, the RFE model can be accessed via the ``rfe_estimator_``
    attribute. Once fitted, the visualizer acts as a wrapper for this
    estimator and not for the original model passed to the model. This way the
    visualizer model can be used to make predictions.

    .. caution:: This visualizer requires a model that has either a ``coef_``
        or ``feature_importances_`` attribute when fitted.
    """

    def __init__(self, model, ax=None, step=1, groups=None, cv=None,
                 scoring=None, **kwargs):

        # Initialize the model visualizer
        super(RFECV, self).__init__(model, ax=ax, **kwargs)

        # Set parameters
        self.set_params(step=step, groups=groups, cv=cv, scoring=scoring)

    def fit(self, X, y=None):
        """
        Fits the RFECV with the wrapped model to the specified data and draws
        the rfecv curve with the optimal number of features found.

        Parameters
        ----------
        X : array-like, shape (n_samples, n_features)
            Training vector, where n_samples is the number of samples and
            n_features is the number of features.

        y : array-like, shape (n_samples) or (n_samples, n_features), optional
            Target relative to X for classification or regression.

        Returns
        -------
        self : instance
            Returns the instance of the RFECV visualizer.
        """
        X, y = check_X_y(X, y, "csr")
        n_features = X.shape[1]

        # This check is kind of unnecessary since RFE will do it, but it's
        # nice to get it out of the way ASAP and raise a meaningful error.
        if 0.0 < self.step < 1.0:
            step = int(max(1, self.step * n_features))
        else:
            step = int(self.step)

        if step < 0:
            raise YellowbrickValueError("step must be >0")

        # Create the RFE model
        rfe = RFE(self.estimator, step=step)
        self.n_feature_subsets_ = np.arange(1, n_features+step, step)

        # Create the cross validation params
        # TODO: handle random state
        cv_params = {
            key: self.get_params()[key]
            for key in ('groups', 'cv', 'scoring')
        }

        # Perform cross-validation for each feature subset
        scores = []
        for n_features_to_select in self.n_feature_subsets_:
            rfe.set_params(n_features_to_select=n_features_to_select)
            scores.append(cross_val_score(rfe, X, y, **cv_params))

        # Convert scores to array
        self.cv_scores_ = np.array(scores)

        # Find the best RFE model
        bestidx = self.cv_scores_.mean(axis=1).argmax()
        self.n_features_ = self.n_feature_subsets_[bestidx]

        # Fit the final RFE model for the number of features
        self.rfe_estimator_ = rfe
        self.rfe_estimator_.set_params(n_features_to_select=self.n_features_)
        self.rfe_estimator_.fit(X, y)

        # Rewrap the visualizer to use the rfe estimator
        self._wrapped = self.rfe_estimator_

        # Hoist the RFE params to the visualizer
        self.support_ = self.rfe_estimator_.support_
        self.ranking_ = self.rfe_estimator_.ranking_

        self.draw()
        return self

    def draw(self, **kwargs):
        """
        Renders the rfecv curve.
        """
        # Compute the curves
        x = self.n_feature_subsets_
        means = self.cv_scores_.mean(axis=1)
        sigmas = self.cv_scores_.std(axis=1)


        # Plot one standard deviation above and below the mean
        self.ax.fill_between(x, means - sigmas, means+sigmas, alpha=0.25)

        # Plot the curve
        self.ax.plot(x, means, 'o-')

        # Plot the maximum number of features
        self.ax.axvline(
            self.n_features_, c='k', ls='--',
            label="n_features = {}\nscore = {:0.3f}".format(
                self.n_features_, self.cv_scores_.mean(axis=1).max()
            )
        )

        return self.ax

    def finalize(self, **kwargs):
        """
        Add the title, legend, and other visual final touches to the plot.
        """
        # Set the title of the figure
        self.set_title('RFECV for {}'.format(self.name))

        # Add the legend
        self.ax.legend(frameon=True, loc='best')

        # Set the axis labels
        self.ax.set_xlabel('Number of Features Selected')
        self.ax.set_ylabel('Score')


##########################################################################
## Quick Methods
##########################################################################

def rfecv(model, X, y, ax=None, step=1, groups=None, cv=None,
          scoring=None, **kwargs):
    """
    Performs recursive feature elimination with cross-validation to determine
    an optimal number of features for a model. Visualizes the feature subsets
    with respect to the cross-validation score.

    This helper function is a quick wrapper to utilize the RFECV visualizer
    for one-off analysis.

    Parameters
    ----------
    model : a scikit-learn estimator
        An object that implements ``fit`` and provides information about the
        relative importance of features with either a ``coef_`` or
        ``feature_importances_`` attribute.

        Note that the object is cloned for each validation.

    X : array-like, shape (n_samples, n_features)
        Training vector, where n_samples is the number of samples and
        n_features is the number of features.

    y : array-like, shape (n_samples) or (n_samples, n_features), optional
        Target relative to X for classification or regression.

    ax : matplotlib.Axes object, optional
        The axes object to plot the figure on.

    step : int or float, optional (default=1)
        If greater than or equal to 1, then step corresponds to the (integer)
        number of features to remove at each iteration. If within (0.0, 1.0),
        then step corresponds to the percentage (rounded down) of features to
        remove at each iteration.

    groups : array-like, with shape (n_samples,), optional
        Group labels for the samples used while splitting the dataset into
        train/test set.

    cv : int, cross-validation generator or an iterable, optional
        Determines the cross-validation splitting strategy.
        Possible inputs for cv are:

          - None, to use the default 3-fold cross-validation,
          - integer, to specify the number of folds.
          - An object to be used as a cross-validation generator.
          - An iterable yielding train/test splits.

        see the scikit-learn
        `cross-validation guide <http://scikit-learn.org/stable/modules/cross_validation.html>`_
        for more information on the possible strategies that can be used here.

    scoring : string, callable or None, optional, default: None
        A string or scorer callable object / function with signature
        ``scorer(estimator, X, y)``. See scikit-learn model evaluation
        documentation for names of possible metrics.

    kwargs : dict
        Keyword arguments that are passed to the base class and may influence
        the visualization as defined in other Visualizers. These arguments are
        also passed to the `poof()` method, e.g. can pass a path to save the
        figure to.

    Returns
    -------
    ax : matplotlib axes
        Returns the axes that the rfecv were drawn on.
    """
    # Initialize the visualizer
    oz = RFECV(model, ax=ax, step=step, groups=groups, cv=cv, scoring=scoring)

    # Fit and poof the visualizer
    oz.fit(X, y)
    oz.poof(**kwargs)
    return oz.ax

yellowbrick/features/rfecv.py
# yellowbrick.features.rfecv
# Visualize the number of features selected with recursive feature elimination
#
# Author:  Benjamin Bengfort <benjamin@bengfort.com>
# Created: Tue Apr 03 17:31:37 2018 -0400
#
# ID: rfecv.py [] benjamin@bengfort.com $

"""
Visualize the number of features selected using recursive feature elimination
"""

##########################################################################
## Imports
##########################################################################

import numpy as np

from yellowbrick.base import ModelVisualizer
from yellowbrick.exceptions import YellowbrickValueError

from sklearn.utils import check_X_y
from sklearn.feature_selection import RFE
from sklearn.model_selection import cross_val_score


##########################################################################
## Recursive Feature Elimination
##########################################################################

class RFECV(ModelVisualizer):
    """
    Recursive Feature Elimination, Cross-Validated (RFECV) feature selection.

    Selects the best subset of features for the supplied estimator by removing
    0 to N features (where N is the number of features) using recursive
    feature elimination, then selecting the best subset based on the
    cross-validation score of the model. Recursive feature elimination
    eliminates n features from a model by fitting the model multiple times and
    at each step, removing the weakest features, determined by either the
    ``coef_`` or ``feature_importances_`` attribute of the fitted model.

    The visualization plots the score relative to each subset and shows trends
    in feature elimination. If the feature elimination CV score is flat, then
    potentially there are not enough features in the model. An ideal curve is
    when the score jumps from low to high as the number of features removed
    increases, then slowly decreases again from the optimal number of
    features.

    Parameters
    ----------
    model : a scikit-learn estimator
        An object that implements ``fit`` and provides information about the
        relative importance of features with either a ``coef_`` or
        ``feature_importances_`` attribute.

        Note that the object is cloned for each validation.

    ax : matplotlib.Axes object, optional
        The axes object to plot the figure on.

    step : int or float, optional (default=1)
        If greater than or equal to 1, then step corresponds to the (integer)
        number of features to remove at each iteration. If within (0.0, 1.0),
        then step corresponds to the percentage (rounded down) of features to
        remove at each iteration.

    groups : array-like, with shape (n_samples,), optional
        Group labels for the samples used while splitting the dataset into
        train/test set.

    cv : int, cross-validation generator or an iterable, optional
        Determines the cross-validation splitting strategy.
        Possible inputs for cv are:

          - None, to use the default 3-fold cross-validation,
          - integer, to specify the number of folds.
          - An object to be used as a cross-validation generator.
          - An iterable yielding train/test splits.

        see the scikit-learn
        `cross-validation guide <http://scikit-learn.org/stable/modules/cross_validation.html>`_
        for more information on the possible strategies that can be used here.

    scoring : string, callable or None, optional, default: None
        A string or scorer callable object / function with signature
        ``scorer(estimator, X, y)``. See scikit-learn model evaluation
        documentation for names of possible metrics.

    kwargs : dict
        Keyword arguments that are passed to the base class and may influence
        the visualization as defined in other Visualizers.

    Attributes
    ----------
    n_features_ : int
        The number of features in the selected subset

    support_ : array of shape [n_features]
        A mask of the selected features

    ranking_ : array of shape [n_features]
        The feature ranking, such that ``ranking_[i]`` corresponds to the
        ranked position of feature i. Selected features are assigned rank 1.

    cv_scores_ : array of shape [n_subsets_of_features, n_splits]
        The cross-validation scores for each subset of features and splits in
        the cross-validation strategy.

    rfe_estimator_ : sklearn.feature_selection.RFE
        A fitted RFE estimator wrapping the original estimator. All estimator
        functions such as ``predict()`` and ``score()`` are passed through to
        this estimator (it rewraps the original model).

    n_feature_subsets_ : array of shape [n_subsets_of_features]
        The number of features removed on each iteration of RFE, computed by the
        number of features in the dataset and the step parameter.

    Notes
    -----
    This model wraps ``sklearn.feature_selection.RFE`` and not
    ``sklearn.feature_selection.RFECV`` because access to the internals of the
    CV and RFE estimators is required for the visualization. The visualizer
    does take similar arguments, however it does not expose the same internal
    attributes.

    Additionally, the RFE model can be accessed via the ``rfe_estimator_``
    attribute. Once fitted, the visualizer acts as a wrapper for this
    estimator and not for the original model passed to the model. This way the
    visualizer model can be used to make predictions.

    .. caution:: This visualizer requires a model that has either a ``coef_``
        or ``feature_importances_`` attribute when fitted.
    """

    def __init__(self, model, ax=None, step=1, groups=None, cv=None,
                 scoring=None, **kwargs):

        # Initialize the model visualizer
        super(RFECV, self).__init__(model, ax=ax, **kwargs)

        # Set parameters
        self.set_params(step=step, groups=groups, cv=cv, scoring=scoring)

    def fit(self, X, y=None):
        """
        Fits the RFECV with the wrapped model to the specified data and draws
        the rfecv curve with the optimal number of features found.

        Parameters
        ----------
        X : array-like, shape (n_samples, n_features)
            Training vector, where n_samples is the number of samples and
            n_features is the number of features.

        y : array-like, shape (n_samples) or (n_samples, n_features), optional
            Target relative to X for classification or regression.

        Returns
        -------
        self : instance
            Returns the instance of the RFECV visualizer.
        """
        X, y = check_X_y(X, y, "csr")
        n_features = X.shape[1]

        # This check is kind of unnecessary since RFE will do it, but it's
        # nice to get it out of the way ASAP and raise a meaningful error.
        if 0.0 < self.step < 1.0:
            step = int(max(1, self.step * n_features))
        else:
            step = int(self.step)

        if step <= 0:
            raise YellowbrickValueError("step must be >0")

        # Create the RFE model
        rfe = RFE(self.estimator, step=step)
        self.n_feature_subsets_ = np.arange(1, n_features+step, step)

        # Create the cross validation params
        # TODO: handle random state
        cv_params = {
            key: self.get_params()[key]
            for key in ('groups', 'cv', 'scoring')
        }

        # Perform cross-validation for each feature subset
        scores = []
        for n_features_to_select in self.n_feature_subsets_:
            rfe.set_params(n_features_to_select=n_features_to_select)
            scores.append(cross_val_score(rfe, X, y, **cv_params))

        # Convert scores to array
        self.cv_scores_ = np.array(scores)

        # Find the best RFE model
        bestidx = self.cv_scores_.mean(axis=1).argmax()
        self.n_features_ = self.n_feature_subsets_[bestidx]

        # Fit the final RFE model for the number of features
        self.rfe_estimator_ = rfe
        self.rfe_estimator_.set_params(n_features_to_select=self.n_features_)
        self.rfe_estimator_.fit(X, y)

        # Rewrap the visualizer to use the rfe estimator
        self._wrapped = self.rfe_estimator_

        # Hoist the RFE params to the visualizer
        self.support_ = self.rfe_estimator_.support_
        self.ranking_ = self.rfe_estimator_.ranking_

        self.draw()
        return self

    def draw(self, **kwargs):
        """
        Renders the rfecv curve.
        """
        # Compute the curves
        x = self.n_feature_subsets_
        means = self.cv_scores_.mean(axis=1)
        sigmas = self.cv_scores_.std(axis=1)


        # Plot one standard deviation above and below the mean
        self.ax.fill_between(x, means - sigmas, means+sigmas, alpha=0.25)

        # Plot the curve
        self.ax.plot(x, means, 'o-')

        # Plot the maximum number of features
        self.ax.axvline(
            self.n_features_, c='k', ls='--',
            label="n_features = {}\nscore = {:0.3f}".format(
                self.n_features_, self.cv_scores_.mean(axis=1).max()
            )
        )

        return self.ax

    def finalize(self, **kwargs):
        """
        Add the title, legend, and other visual final touches to the plot.
        """
        # Set the title of the figure
        self.set_title('RFECV for {}'.format(self.name))

        # Add the legend
        self.ax.legend(frameon=True, loc='best')

        # Set the axis labels
        self.ax.set_xlabel('Number of Features Selected')
        self.ax.set_ylabel('Score')


##########################################################################
## Quick Methods
##########################################################################

def rfecv(model, X, y, ax=None, step=1, groups=None, cv=None,
          scoring=None, **kwargs):
    """
    Performs recursive feature elimination with cross-validation to determine
    an optimal number of features for a model. Visualizes the feature subsets
    with respect to the cross-validation score.

    This helper function is a quick wrapper to utilize the RFECV visualizer
    for one-off analysis.

    Parameters
    ----------
    model : a scikit-learn estimator
        An object that implements ``fit`` and provides information about the
        relative importance of features with either a ``coef_`` or
        ``feature_importances_`` attribute.

        Note that the object is cloned for each validation.

    X : array-like, shape (n_samples, n_features)
        Training vector, where n_samples is the number of samples and
        n_features is the number of features.

    y : array-like, shape (n_samples) or (n_samples, n_features), optional
        Target relative to X for classification or regression.

    ax : matplotlib.Axes object, optional
        The axes object to plot the figure on.

    step : int or float, optional (default=1)
        If greater than or equal to 1, then step corresponds to the (integer)
        number of features to remove at each iteration. If within (0.0, 1.0),
        then step corresponds to the percentage (rounded down) of features to
        remove at each iteration.

    groups : array-like, with shape (n_samples,), optional
        Group labels for the samples used while splitting the dataset into
        train/test set.

    cv : int, cross-validation generator or an iterable, optional
        Determines the cross-validation splitting strategy.
        Possible inputs for cv are:

          - None, to use the default 3-fold cross-validation,
          - integer, to specify the number of folds.
          - An object to be used as a cross-validation generator.
          - An iterable yielding train/test splits.

        see the scikit-learn
        `cross-validation guide <http://scikit-learn.org/stable/modules/cross_validation.html>`_
        for more information on the possible strategies that can be used here.

    scoring : string, callable or None, optional, default: None
        A string or scorer callable object / function with signature
        ``scorer(estimator, X, y)``. See scikit-learn model evaluation
        documentation for names of possible metrics.

    kwargs : dict
        Keyword arguments that are passed to the base class and may influence
        the visualization as defined in other Visualizers. These arguments are
        also passed to the `poof()` method, e.g. can pass a path to save the
        figure to.

    Returns
    -------
    ax : matplotlib axes
        Returns the axes that the rfecv were drawn on.
    """
    # Initialize the visualizer
    oz = RFECV(model, ax=ax, step=step, groups=groups, cv=cv, scoring=scoring)

    # Fit and poof the visualizer
    oz.fit(X, y)
    oz.poof(**kwargs)
    return oz.ax

yellowbrick/features/rfecv.py
diff --git a/docs/contributing/advanced_development_topics.rst b/docs/contributing/advanced_development_topics.rst
index 6ebaa8bf0..6f32446c2 100644
--- a/docs/contributing/advanced_development_topics.rst
+++ b/docs/contributing/advanced_development_topics.rst
@@ -72,9 +72,9 @@ Testing Conventions
 
     Ensure there is at least one image comparison test per visualizer. This is the primary regression testing of Yellowbrick and these tests catch a lot when changes occur in our dependencies or environment.
 
-- Use pytest assertions rather than unittest.
+- Use pytest assertions rather than ``unittest.TestCase`` methods.
 
-    We prefer ``assert 2+2 == 4`` rather than ``self.assertEquals(2+2, 4)``. Though there is a lot of legacy unittest assertions, we've moved to pytest and one day believe we will have removed the unittest dependency.
+    We prefer ``assert 2+2 == 4`` rather than ``self.assertEquals(2+2, 4)``. As a result, test classes should not extend ``unittest.Testcase`` but should extend the ``VisualTestCase`` in the tests package. Note that if you're writing tests that do not generate matplotlib figures you can simply extend ``object``.
 
 - Use test fixtures and sklearn dataset generators.
 
diff --git a/docs/contributing/developing_visualizers.rst b/docs/contributing/developing_visualizers.rst
index 6a845d46d..937144912 100644
--- a/docs/contributing/developing_visualizers.rst
+++ b/docs/contributing/developing_visualizers.rst
@@ -86,26 +86,21 @@ Testing
 
 The test package mirrors the yellowbrick package in structure and also contains several helper methods and base functionality. To add a test to your visualizer, find the corresponding file to add the test case, or create a new test file in the same place you added your code.
 
-Visual tests are notoriously difficult to create --- how do you test a visualization or figure? Moreover, testing scikit-learn models with real data can consume a lot of memory. Therefore the primary test you should create is simply to test your visualizer from end to end and make sure that no exceptions occur. To assist with this, we have two primary helpers, ``VisualTestCase`` and ``DatasetMixin``. Create your unittest as follows::
+Visual tests are notoriously difficult to create --- how do you test a visualization or figure? Moreover, testing scikit-learn models with real data can consume a lot of memory. Therefore the primary test you should create is simply to test your visualizer from end to end and make sure that no exceptions occur. To assist with this, we have two primary helpers, ``VisualTestCase`` and ``DatasetMixin``. Create your tests as follows::
 
     import pytest
+
     from tests.base import VisualTestCase
-    from tests.dataset import DatasetMixin
+    from yellowbrick.datasets import load_occupancy
 
-    class MyVisualizerTests(VisualTestCase, DatasetMixin):
+    class MyVisualizerTests(VisualTestCase):
 
         def test_my_visualizer(self):
             """
             Test MyVisualizer on a real dataset
             """
-            # Load the data from the fixture
-            dataset = self.load_data('occupancy')
-
-            # Get the data
-            X = dataset[[
-                "temperature", "relative_humidity", "light", "C02", "humidity"
-            ]]
-            y = dataset['occupancy'].astype(int)
+            # Load the occupancy dataset
+            X, y = load_occupancy()
 
             try:
                 visualizer = MyVisualizer()
@@ -114,6 +109,7 @@ Visual tests are notoriously difficult to create --- how do you test a visualiza
             except Exception as e:
                 pytest.fail("my visualizer didn't work")
 
+
 Running the Test Suite
 ----------------------
 
@@ -136,7 +132,7 @@ Writing an image based comparison test is only a little more difficult than the
 
 The main consideration is that you must specify the “baseline”, or expected, image in the ``tests/baseline_images/`` folder structure.
 
-For example, create your unittest located in ``tests/test_regressor/test_myvisualizer.py`` as follows::
+For example, create your test function located in ``tests/test_regressor/test_myvisualizer.py`` as follows::
 
     from tests.base import VisualTestCase
     ...
@@ -248,7 +244,7 @@ This is a pretty good structure for a documentation page; a brief introduction f
 
 At this point there are several places where you can list your visualizer, but to ensure it is included in the documentation it *must be listed in the TOC of the local index*. Find the ``index.rst`` file in your subdirectory and add your rst file (without the ``.rst`` extension) to the ``..toctree::`` directive. This will ensure the documentation is included when it is built.
 
-Building the Docs 
+Building the Docs
 ~~~~~~~~~~~~~~~~~
 
 Speaking of, you can build your documentation by changing into the ``docs`` directory and running ``make html``, the documentation will be built and rendered in the ``_build/html`` directory. You can view it by opening ``_build/html/index.html`` then navigating to your documentation in the browser.
diff --git a/tests/README.md b/tests/README.md
index dee805042..f22f58eb0 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -2,7 +2,7 @@
 
 *Welcome to the Yellowbrick tests!*
 
-If you're looking for information about how to use Yellowbrick, for our contributor's guide, for examples and teaching resources, for answers to frequently asked questions, and more, please visit the latest version of our documentation at [www.scikit-yb.org](https://www.scikit-yb.org/). 
+If you're looking for information about how to use Yellowbrick, for our contributor's guide, for examples and teaching resources, for answers to frequently asked questions, and more, please visit the latest version of our documentation at [www.scikit-yb.org](https://www.scikit-yb.org/).
 
 ## Running Yellowbrick Tests
 
@@ -20,7 +20,7 @@ Tests can then be run as follows from the project `root`:
 $ make test
 ```
 
-The Makefile uses the `pytest` runner and testing suite as well as the coverage library. 
+The Makefile uses the `pytest` runner and testing suite as well as the coverage library.
 
 ## Adding a Test for Your Visualizer
 
@@ -28,11 +28,11 @@ The `tests` package mirrors the yellowbrick package in structure and also contai
 
 ### Visual Tests
 
-The primary test you should create is simply to test your visualizer from end to end and make sure that no exceptions occur. 
+The primary test you should create is simply to test your visualizer from end to end and make sure that no exceptions occur.
 
-Visual tests are notoriously difficult to create --- how do you test a visualization or figure? Moreover, testing scikit-learn models with real data can consume a lot of memory. To assist with this, we have two primary helpers, `VisualTestCase` and the `yellowbrick.datasets` module. 
+Visual tests are notoriously difficult to create --- how do you test a visualization or figure? Moreover, testing scikit-learn models with real data can consume a lot of memory. To assist with this, we have two primary helpers, `VisualTestCase` and the `yellowbrick.datasets` module.
 
-Leverage these helpers to create your unittest as follows:
+Leverage these helpers to create your tests as follows:
 
 ```python
 import pytest
@@ -64,7 +64,7 @@ Writing an image-based comparison test is only a little more difficult than the
 
 The main consideration is that you must specify the “baseline” (i.e. expected) image in the `tests/baseline_images/` folder structure.
 
-For example, let's say you create your unittest in `tests/test_regressor/test_myvisualizer.py` as follows:
+For example, let's say you create your tests in `tests/test_regressor/test_myvisualizer.py` as follows:
 
 ```python
 from tests.base import VisualTestCase
diff --git a/tests/__init__.py b/tests/__init__.py
index 134c73e0f..d6b063c7c 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -17,7 +17,6 @@
 ## Imports
 ##########################################################################
 
-import unittest
 import matplotlib
 
 ## IMPORTANT! Set matplotlib to use the Agg backend before imported anywhere!
@@ -35,13 +34,13 @@
 ## Initialization Tests
 ##########################################################################
 
-class InitializationTests(unittest.TestCase):
+class TestInitialization(object):
 
     def test_sanity(self):
         """
         Test that tests work by confirming 7-3 = 4
         """
-        self.assertEqual(7-3, 4, "The world went wrong!!")
+        assert 7 - 3 == 4, "The world went wrong!!"
 
     def test_import(self):
         """
@@ -58,6 +57,6 @@ def test_version(self):
         """
         try:
             import yellowbrick as yb
-            self.assertEqual(yb.__version__, EXPECTED_VERSION)
+            assert yb.__version__ == EXPECTED_VERSION
         except ImportError:
             self.fail("Could not import the yellowbrick library!")
diff --git a/tests/base.py b/tests/base.py
index 869b893eb..7899f24d5 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -15,24 +15,31 @@
 ##########################################################################
 
 import os
-import inspect
 import sys
+import inspect
 
-import unittest
 import matplotlib as mpl
 import matplotlib.pyplot as plt
 
 from matplotlib import ticker
-from matplotlib import rcParams
-
 from matplotlib.testing.compare import compare_images
 from yellowbrick.exceptions import ImageComparisonFailure
 
+
+##########################################################################
+## Environment
+##########################################################################
+
 def is_windows_or_conda():
+    """
+    Simple detection mechanism to determine if the tests are running in a
+    win32 or Anaconda/Miniconda environment.
+    """
     is_windows = sys.platform == 'win32'
     is_conda = os.path.exists(os.path.join(sys.prefix, 'conda-meta'))
     return is_windows or is_conda
 
+
 ##########################################################################
 ## Module Constants
 ##########################################################################
@@ -43,45 +50,45 @@ def is_windows_or_conda():
 BASELINE_IMAGES = os.path.join(TESTS, "baseline_images")
 IS_WINDOWS_OR_CONDA = is_windows_or_conda()
 
+
 ##########################################################################
 ## Visual Test Case
 ##########################################################################
 
-class VisualTestCase(unittest.TestCase):
-
-    @classmethod
-    def setUpClass(klass):
-        """
-        This setup function is available to ensure that all CI tests
-        that do visual work are set up correctly.
+class VisualTestCase(object):
+    """
+    The visual test case class ensures that all tests inside of the class
+    can execute image similarity tests inside of a clean matplotlib global
+    figure.
+    """
 
-        Note:
+    def setup_method(self):
         """
-        super(VisualTestCase, klass).setUpClass()
+        Before a visual test case method is run, ensure that the previous
+        figure is closed and the current axes are cleared.
 
-    def setUp(self):
-        """
-        Close all previous plots
+        See: https://docs.pytest.org/en/latest/xunit_setup.html
         """
         # Reset the matplotlib environment
-        plt.cla()        # clear current axis
-        plt.clf()        # clear current figure
-        plt.close("all") # close all existing plots
+        plt.cla()         # clear current axis
+        plt.clf()         # clear current figure
+        plt.close("all")  # close all existing plots
 
-        # Travis-CI does not have san-serif
-        rcParams['font.family'] = 'DejaVu Sans'
+        # Travis-CI does not have san-serif so ensure standard fonts are used.
+        # Note that this must be set before each test otherwise it will be reset by
+        # the Yellowbrick styles.
+        mpl.rcParams['font.family'] = 'DejaVu Sans'
 
-        super(VisualTestCase, self).setUp()
-
-    def assert_images_similar(self, visualizer=None, ax=None, tol=0.01, windows_tol=None,  **kwargs):
+    def assert_images_similar(self, visualizer=None, ax=None,
+                              tol=0.01, windows_tol=None, **kwargs):
         """Accessible testing method for testing generation of a Visualizer.
 
         Requires the placement of a baseline image for comparison in the
         tests/baseline_images folder that corresponds to the module path of the
         VisualTestCase being evaluated. The name of the image corresponds to
-        the unittest function where "self.assert_images_similar" is called.
+        the test function where "self.assert_images_similar" is called.
 
-        For example, calling "assert_images_similar" in the unittest
+        For example, calling "assert_images_similar" in the test function
         "test_class_report" in tests.test_classifier.test_class_balance would
         require placement a baseline image at:
 
@@ -93,20 +100,21 @@ def assert_images_similar(self, visualizer=None, ax=None, tol=0.01, windows_tol=
 
         actual_images/
 
-        visualizer : yellowbrick visualizer
+        visualizer : yellowbrick visualizer, default: None
             An instantiated yellowbrick visualizer that has been fitted,
             transformed and had all operations except for poof called on it.
 
         ax : matplotlib Axes, default: None
             The axis to plot the figure on.
 
-        tol : float
+        tol : float, default: 0.01
             The tolerance (a color value difference, where 255 is the
             maximal difference).  The test fails if the average pixel
             difference is greater than this value.
 
         windows_tol: float, default: None
-            Similar to the tol parameter, but targeted for testing on a windows environment. 
+            Similar to the tol parameter, but targeted for testing on a
+            windows environment.
 
         kwargs : dict
             Options to pass to the ImageComparison class.
@@ -116,7 +124,12 @@ def assert_images_similar(self, visualizer=None, ax=None, tol=0.01, windows_tol=
 
         # Build and execute the image comparison
         compare = ImageComparison(
-            inspect.stack(), visualizer=visualizer, ax=ax, tol=tol, windows_tol=windows_tol, **kwargs
+            inspect.stack(),
+            visualizer=visualizer,
+            ax=ax,
+            tol=tol,
+            windows_tol=windows_tol,
+            **kwargs
         )
         compare()
 
@@ -182,8 +195,8 @@ class ImageComparison(object):
     ValueError : at least one of visualizer or ax must be specified.
     """
 
-    def __init__(self, stack, visualizer=None, ax=None, tol=0.01, 
-                 windows_tol=0.01, ext=".png", remove_ticks=True, 
+    def __init__(self, stack, visualizer=None, ax=None, tol=0.01,
+                 windows_tol=0.01, ext=".png", remove_ticks=True,
                  remove_title=True, remove_labels=True, remove_legend=True):
 
         # Ensure we have something to draw on
@@ -216,10 +229,9 @@ def __init__(self, stack, visualizer=None, ax=None, tol=0.01,
         # Set the error tolerance depending on the os
         if os.name == "nt" and windows_tol is not None:
             self.tol = windows_tol
-        else:        
+        else:
             self.tol = tol
 
-
         # Save other image comparison properties
         self.ext = ext
         self.remove_ticks = remove_ticks
diff --git a/tests/conftest.py b/tests/conftest.py
index d6ffcbe98..b52965bb8 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -18,9 +18,34 @@
 ##########################################################################
 
 import os
+import matplotlib as mpl
 
 from pytest_flakes import FlakesItem
 
+
+##########################################################################
+## Configure tests
+##########################################################################
+
+def pytest_configure(config):
+    """
+    This function is called by pytest for every plugin and conftest file
+    after the command line arguments have been passed but before the
+    session object is created and all of the tests are created. It is used
+    to set a global configuration before all tests are run.
+
+    Yellowbrick uses this function primarily to ensure that the matplotlib
+    environment is setup correctly for all tests.
+    """
+    # This is redundant with the line in tests/__init__.py but ensures that
+    # the backend is correctly set across all tests and plugins.
+    mpl.use('Agg')
+
+    # Travis-CI does not have san-serif so ensure standard fonts are used.
+    # TODO: this is currently being reset before each test; needs fixing.
+    mpl.rcParams['font.family'] = 'DejaVu Sans'
+
+
 ##########################################################################
 ## PyTest Hooks
 ##########################################################################
diff --git a/tests/dataset.py b/tests/dataset.py
index 5354d912a..544f9664b 100644
--- a/tests/dataset.py
+++ b/tests/dataset.py
@@ -105,7 +105,7 @@
 
 class DatasetMixin(object):
     """
-    Mixin for unittest.TestCase class to download datasets from S3 for
+    Mixin for VisualTestCase class to download datasets from S3 for
     testing real world machine learning visual diagnostics.
     """
 
diff --git a/tests/requirements.txt b/tests/requirements.txt
index f88565d87..5456d0538 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -15,7 +15,7 @@ requests>=2.18.3
 
 # Optional Testing Dependencies
 nltk>=3.2
-# spacy>=2.0.18 
+# spacy>=2.0.18
 pandas>=0.20
 umap-learn==0.3
 numba==0.42
diff --git a/tests/test_base.py b/tests/test_base.py
index d011e4d34..119619e9d 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -31,6 +31,7 @@
 
 from sklearn.datasets import make_classification
 
+
 ##########################################################################
 ## Base Cases
 ##########################################################################
diff --git a/tests/test_bestfit.py b/tests/test_bestfit.py
index 8641b4a91..77476ee65 100644
--- a/tests/test_bestfit.py
+++ b/tests/test_bestfit.py
@@ -35,49 +35,49 @@
 ## Best fit tests
 ##########################################################################
 
-class BestFitTests(VisualTestCase):
+class TestBestFit(VisualTestCase):
 
     def test_bad_estimator(self):
         """
         Test that a bad estimator name raises a value error.
         """
-        fig, axe = plt.subplots()
+        fig, ax = plt.subplots()
         X, y = ANSCOMBE[1]
 
-        with self.assertRaises(YellowbrickValueError):
-            draw_best_fit(X, y, axe, 'pepper')
+        with pytest.raises(YellowbrickValueError):
+            draw_best_fit(X, y, ax, 'pepper')
 
     def test_ensure_same_length(self):
         """
         Ensure that vectors of different lengths raise
         """
-        fig, axe = plt.subplots()
+        fig, ax = plt.subplots()
         X = np.array([1, 2, 3, 5, 8, 10, 2])
         y = np.array([1, 3, 6, 2])
 
-        with self.assertRaises(YellowbrickValueError):
-            draw_best_fit(X, y, axe, 'linear')
+        with pytest.raises(YellowbrickValueError):
+            draw_best_fit(X, y, ax, 'linear')
 
-        with self.assertRaises(YellowbrickValueError):
-            draw_best_fit(X[:,np.newaxis], y, axe, 'linear')
+        with pytest.raises(YellowbrickValueError):
+            draw_best_fit(X[:,np.newaxis], y, ax, 'linear')
 
     @pytest.mark.filterwarnings('ignore')
-    def testdraw_best_fit(self):
+    def test_draw_best_fit(self):
         """
         Test that drawing a best fit line works.
         """
-        fig, axe = plt.subplots()
+        fig, ax = plt.subplots()
         X, y = ANSCOMBE[0]
 
-        self.assertEqual(axe, draw_best_fit(X, y, axe, 'linear'))
-        self.assertEqual(axe, draw_best_fit(X, y, axe, 'quadratic'))
+        assert ax == draw_best_fit(X, y, ax, 'linear')
+        assert ax == draw_best_fit(X, y, ax, 'quadratic')
 
 
 ##########################################################################
 ## Estimator tests
 ##########################################################################
 
-class EstimatorTests(VisualTestCase):
+class TestEstimator(VisualTestCase):
     """
     Test the estimator functions for best fit lines.
     """
@@ -92,9 +92,8 @@ def test_linear(self):
         X = X[:,np.newaxis]
 
         model = fit_linear(X, y)
-        self.assertIsNotNone(model)
-        self.assertIsInstance(model, LinearRegression)
-
+        assert model is not None
+        assert isinstance(model, LinearRegression)
 
     def test_quadratic(self):
         """
@@ -106,8 +105,8 @@ def test_quadratic(self):
         X = X[:,np.newaxis]
 
         model = fit_quadratic(X, y)
-        self.assertIsNotNone(model)
-        self.assertIsInstance(model, Pipeline)
+        assert model is not None
+        assert isinstance(model, Pipeline)
 
     def test_select_best(self):
         """
@@ -119,8 +118,8 @@ def test_select_best(self):
         X = X[:,np.newaxis]
 
         model = fit_select_best(X, y)
-        self.assertIsNotNone(model)
-        self.assertIsInstance(model, Pipeline)
+        assert model is not None
+        assert isinstance(model, Pipeline)
 
         X, y = ANSCOMBE[3]
         X = np.array(X)
@@ -128,5 +127,5 @@ def test_select_best(self):
         X = X[:,np.newaxis]
 
         model = fit_select_best(X, y)
-        self.assertIsNotNone(model)
-        self.assertIsInstance(model, LinearRegression)
+        assert model is not None
+        assert isinstance(model, LinearRegression)
diff --git a/tests/test_classifier/__init__.py b/tests/test_classifier/__init__.py
index c82b7f039..a07b46aea 100644
--- a/tests/test_classifier/__init__.py
+++ b/tests/test_classifier/__init__.py
@@ -1,6 +1,13 @@
-#Backend must be set before first use. 
-# Setting backend here allows us to run tests just in this folder, without running the whole yellowbrick.tests folder
-# This command will have no effect if backend has already been set previously. 
-import matplotlib
-matplotlib.use('Agg')
+# tests.test_classifier
+# Tests for the classifier visualizers
+#
+# ID: __init__.py [] benjamin@bengfort.com $
+
+"""
+Tests for the classifier visualizers
+"""
+
+##########################################################################
+## Imports
+##########################################################################
 
diff --git a/tests/test_classifier/test_class_prediction_error.py b/tests/test_classifier/test_class_prediction_error.py
index 826cf3ff0..341e2f4a6 100644
--- a/tests/test_classifier/test_class_prediction_error.py
+++ b/tests/test_classifier/test_class_prediction_error.py
@@ -45,7 +45,7 @@
 ##########################################################################
 
 
-class ClassPredictionErrorTests(VisualTestCase, DatasetMixin):
+class TestClassPredictionError(VisualTestCase, DatasetMixin):
 
     def test_integration_class_prediction_error(self):
         """
@@ -79,7 +79,7 @@ def test_classes_greater_than_indices(self):
         """
         model = LinearSVC()
         model.fit(X, y)
-        with self.assertRaises(ModelError):
+        with pytest.raises(ModelError):
             visualizer = ClassPredictionError(
                 model, classes=["A", "B", "C", "D", "E"]
             )
@@ -91,7 +91,7 @@ def test_classes_less_than_indices(self):
         """
         model = LinearSVC()
         model.fit(X, y)
-        with self.assertRaises(NotImplementedError):
+        with pytest.raises(NotImplementedError):
             visualizer = ClassPredictionError(model, classes=["A"])
             visualizer.score(X, y)
 
@@ -109,7 +109,7 @@ def test_class_type(self):
         X, y = make_multilabel_classification()
         model = RandomForestClassifier()
         model.fit(X, y)
-        with self.assertRaises(YellowbrickValueError):
+        with pytest.raises(YellowbrickValueError):
             visualizer = ClassPredictionError(model)
             visualizer.score(X, y)
 
diff --git a/tests/test_classifier/test_classification_report.py b/tests/test_classifier/test_classification_report.py
index 1badee928..52c8b9b69 100644
--- a/tests/test_classifier/test_classification_report.py
+++ b/tests/test_classifier/test_classification_report.py
@@ -44,7 +44,7 @@
 ##########################################################################
 
 @pytest.mark.usefixtures("binary", "multiclass")
-class ClassificationReportTests(VisualTestCase, DatasetMixin):
+class TestClassificationReport(VisualTestCase, DatasetMixin):
     """
     ClassificationReport visualizer tests
     """
diff --git a/tests/test_classifier/test_confusion_matrix.py b/tests/test_classifier/test_confusion_matrix.py
index 7f43beb65..3a3170264 100644
--- a/tests/test_classifier/test_confusion_matrix.py
+++ b/tests/test_classifier/test_confusion_matrix.py
@@ -68,7 +68,7 @@ def digits(request):
 ##########################################################################
 
 @pytest.mark.usefixtures("digits")
-class ConfusionMatrixTests(VisualTestCase):
+class TestConfusionMatrix(VisualTestCase):
     """
     Test ConfusionMatrix visualizer
     """
diff --git a/tests/test_classifier/test_rocauc.py b/tests/test_classifier/test_rocauc.py
index dccf2666a..a136ee3d3 100644
--- a/tests/test_classifier/test_rocauc.py
+++ b/tests/test_classifier/test_rocauc.py
@@ -52,12 +52,29 @@ class FakeClassifier(BaseEstimator, ClassifierMixin):
     pass
 
 
+def assert_valid_rocauc_scores(visualizer, nscores=4):
+    """
+    Assertion helper to ensure scores are correctly computed
+    """
+    __tracebackhide__ = True
+    assert len(visualizer.fpr.keys()) == nscores
+    assert len(visualizer.tpr.keys()) == nscores
+    assert len(visualizer.roc_auc.keys()) == nscores
+
+    for k in (0, 1, "micro", "macro"):
+        assert k in visualizer.fpr
+        assert k in visualizer.tpr
+        assert k in visualizer.roc_auc
+        assert len(visualizer.fpr[k]) == len(visualizer.tpr[k])
+        assert 0.0 < visualizer.roc_auc[k] < 1.0
+
+
 ##########################################################################
 ##  Tests
 ##########################################################################
 
 @pytest.mark.usefixtures("binary", "multiclass")
-class ROCAUCTests(VisualTestCase, DatasetMixin):
+class TestROCAUC(VisualTestCase, DatasetMixin):
 
     def test_binary_probability(self):
         """
@@ -74,20 +91,10 @@ def test_binary_probability(self):
         assert 0 <= s <= 1
 
         # Check the scores
-        self.assertEqual(len(visualizer.fpr.keys()), 4)
-        self.assertEqual(len(visualizer.tpr.keys()), 4)
-        self.assertEqual(len(visualizer.roc_auc.keys()), 4)
-
-        for k in (0, 1, "micro", "macro"):
-            self.assertIn(k, visualizer.fpr)
-            self.assertIn(k, visualizer.tpr)
-            self.assertIn(k, visualizer.roc_auc)
-            self.assertEqual(len(visualizer.fpr[k]), len(visualizer.tpr[k]))
-            self.assertGreater(visualizer.roc_auc[k], 0.0)
-            self.assertLess(visualizer.roc_auc[k], 1.0)
+        assert_valid_rocauc_scores(visualizer)
 
         # Compare the images
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=TOL)
 
     def test_binary_probability_decision(self):
@@ -105,20 +112,10 @@ def test_binary_probability_decision(self):
         assert 0 <= s <= 1
 
         # Check the scores
-        self.assertEqual(len(visualizer.fpr.keys()), 4)
-        self.assertEqual(len(visualizer.tpr.keys()), 4)
-        self.assertEqual(len(visualizer.roc_auc.keys()), 4)
-
-        for k in (0, 1, "micro", "macro"):
-            self.assertIn(k, visualizer.fpr)
-            self.assertIn(k, visualizer.tpr)
-            self.assertIn(k, visualizer.roc_auc)
-            self.assertEqual(len(visualizer.fpr[k]), len(visualizer.tpr[k]))
-            self.assertGreater(visualizer.roc_auc[k], 0.0)
-            self.assertLess(visualizer.roc_auc[k], 1.0)
+        assert_valid_rocauc_scores(visualizer)
 
         # Compare the images
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=TOL)
 
     def test_binary_decision(self):
@@ -136,13 +133,13 @@ def test_binary_decision(self):
         assert 0 <= s <= 1
 
         # Check the scores
-        self.assertEqual(len(visualizer.fpr.keys()), 1)
-        self.assertEqual(len(visualizer.tpr.keys()), 1)
-        self.assertEqual(len(visualizer.roc_auc.keys()), 1)
+        assert len(visualizer.fpr.keys()) == 1
+        assert len(visualizer.tpr.keys()) == 1
+        assert len(visualizer.roc_auc.keys()) == 1
 
         # Compare the images
         # NOTE: increased tolerance for both AppVeyor and Travis CI tests
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=10)
 
     def test_binary_micro_error(self):
@@ -154,7 +151,7 @@ def test_binary_micro_error(self):
         visualizer.fit(self.binary.X.train, self.binary.y.train)
 
         # Ensure score raises error (micro curves aren't defined for binary decisions)
-        with self.assertRaises(ModelError):
+        with pytest.raises(ModelError):
             visualizer.score(self.binary.X.test, self.binary.y.test)
 
     def test_binary_macro_error(self):
@@ -166,7 +163,7 @@ def test_binary_macro_error(self):
         visualizer.fit(self.binary.X.train, self.binary.y.train)
 
         # Ensure score raises error (macro curves aren't defined for binary decisions)
-        with self.assertRaises(ModelError):
+        with pytest.raises(ModelError):
             visualizer.score(self.binary.X.test, self.binary.y.test)
 
     def test_binary_per_class_error(self):
@@ -178,7 +175,7 @@ def test_binary_per_class_error(self):
         visualizer.fit(self.binary.X.train, self.binary.y.train)
 
         # Ensure score raises error (per_class curves not defined for binary decisions)
-        with self.assertRaises(ModelError):
+        with pytest.raises(ModelError):
             visualizer.score(self.binary.X.test, self.binary.y.test)
 
     def test_multiclass_rocauc(self):
@@ -196,20 +193,10 @@ def test_multiclass_rocauc(self):
         assert 0 <= s <= 1
 
         # Check the scores
-        self.assertEqual(len(visualizer.fpr.keys()), 8)
-        self.assertEqual(len(visualizer.tpr.keys()), 8)
-        self.assertEqual(len(visualizer.roc_auc.keys()), 8)
-
-        for k in (0, 1, "micro", "macro"):
-            self.assertIn(k, visualizer.fpr)
-            self.assertIn(k, visualizer.tpr)
-            self.assertIn(k, visualizer.roc_auc)
-            self.assertEqual(len(visualizer.fpr[k]), len(visualizer.tpr[k]))
-            self.assertGreater(visualizer.roc_auc[k], 0.0)
-            self.assertLess(visualizer.roc_auc[k], 1.0)
+        assert_valid_rocauc_scores(visualizer, nscores=8)
 
         # Compare the images
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=TOL)
 
     def test_rocauc_quickmethod(self):
@@ -232,15 +219,15 @@ def test_rocauc_no_micro(self):
 
         # Score the visualizer (should be the macro average)
         s = visualizer.score(self.binary.X.test, self.binary.y.test)
-        self.assertAlmostEqual(s, 0.8)
+        assert s == pytest.approx(0.8)
 
         # Assert that there is no micro score
-        self.assertNotIn("micro", visualizer.fpr)
-        self.assertNotIn("micro", visualizer.tpr)
-        self.assertNotIn("micro", visualizer.roc_auc)
+        assert "micro" not in visualizer.fpr
+        assert "micro" not in visualizer.tpr
+        assert "micro" not in visualizer.roc_auc
 
         # Compare the images
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=TOL)
 
     def test_rocauc_no_macro(self):
@@ -253,15 +240,15 @@ def test_rocauc_no_macro(self):
 
         # Score the visualizer (should be the micro average)
         s = visualizer.score(self.binary.X.test, self.binary.y.test)
-        self.assertAlmostEqual(s, 0.8)
+        assert s == pytest.approx(0.8)
 
         # Assert that there is no macro score
-        self.assertNotIn("macro", visualizer.fpr)
-        self.assertNotIn("macro", visualizer.tpr)
-        self.assertNotIn("macro", visualizer.roc_auc)
+        assert "macro" not in visualizer.fpr
+        assert "macro" not in visualizer.tpr
+        assert "macro" not in visualizer.roc_auc
 
         # Compare the images
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=TOL)
 
     def test_rocauc_no_macro_no_micro(self):
@@ -274,20 +261,20 @@ def test_rocauc_no_macro_no_micro(self):
 
         # Score the visualizer (should be the F1 score)
         s = visualizer.score(self.binary.X.test, self.binary.y.test)
-        self.assertAlmostEqual(s, 0.8)
+        assert s == pytest.approx(0.8)
 
         # Assert that there is no macro score
-        self.assertNotIn("macro", visualizer.fpr)
-        self.assertNotIn("macro", visualizer.tpr)
-        self.assertNotIn("macro", visualizer.roc_auc)
+        assert "macro" not in visualizer.fpr
+        assert "macro" not in visualizer.tpr
+        assert "macro" not in visualizer.roc_auc
 
         # Assert that there is no micro score
-        self.assertNotIn("micro", visualizer.fpr)
-        self.assertNotIn("micro", visualizer.tpr)
-        self.assertNotIn("micro", visualizer.roc_auc)
+        assert "micro" not in visualizer.fpr
+        assert "micro" not in visualizer.tpr
+        assert "micro" not in visualizer.roc_auc
 
         # Compare the images
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=TOL)
 
     def test_rocauc_no_classes(self):
@@ -300,16 +287,16 @@ def test_rocauc_no_classes(self):
 
         # Score the visualizer (should be the micro average)
         s = visualizer.score(self.binary.X.test, self.binary.y.test)
-        self.assertAlmostEqual(s, 0.8)
+        assert s == pytest.approx(0.8)
 
         # Assert that there still are per-class scores
         for c in (0, 1):
-            self.assertIn(c, visualizer.fpr)
-            self.assertIn(c, visualizer.tpr)
-            self.assertIn(c, visualizer.roc_auc)
+            assert c in visualizer.fpr
+            assert c in visualizer.tpr
+            assert c in visualizer.roc_auc
 
         # Compare the images
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=TOL)
 
     def test_rocauc_no_curves(self):
@@ -336,7 +323,7 @@ def test_rocauc_label_encoded(self):
 
         # Score the visualizer
         visualizer.score(self.multiclass.X.test, self.multiclass.y.test)
-        self.assertEqual(list(visualizer.classes_), class_labels)
+        assert list(visualizer.classes_) == class_labels
 
     def test_rocauc_not_label_encoded(self):
         """
@@ -352,7 +339,7 @@ def test_rocauc_not_label_encoded(self):
         visualizer.fit(self.multiclass.X.train, y_train)
 
         # Confirm that y_train and y_test have the same targets before calling score
-        self.assertEqual(set(y_train), set(y_test))
+        assert set(y_train) == set(y_test)
 
     def test_binary_decision_function_rocauc(self):
         """
@@ -360,7 +347,7 @@ def test_binary_decision_function_rocauc(self):
         """
         # Load the model and assert there is no predict_proba method.
         model = LinearSVC()
-        with self.assertRaises(AttributeError):
+        with pytest.raises(AttributeError):
             model.predict_proba
 
         # Fit model and visualizer
@@ -384,7 +371,7 @@ def test_multi_decision_function_rocauc(self):
         """
         # Load the model and assert there is no predict_proba method.
         model = LinearSVC()
-        with self.assertRaises(AttributeError):
+        with pytest.raises(AttributeError):
             model.predict_proba
 
         # Fit model and visualizer
@@ -412,7 +399,7 @@ def test_predict_proba_rocauc(self):
         """
         # Load the model and assert there is no decision_function method.
         model = GaussianNB()
-        with self.assertRaises(AttributeError):
+        with pytest.raises(AttributeError):
             model.decision_function
 
         # Fit model and visualizer
@@ -444,5 +431,5 @@ def test_no_scoring_function(self):
         Test ROCAUC with classifiers that have no scoring method
         """
         visualizer = ROCAUC(FakeClassifier())
-        with self.assertRaises(ModelError):
+        with pytest.raises(ModelError):
             visualizer._get_y_scores(self.binary.X.train)
diff --git a/tests/test_classifier/test_threshold.py b/tests/test_classifier/test_threshold.py
index 8a4907777..3e06ae6b9 100644
--- a/tests/test_classifier/test_threshold.py
+++ b/tests/test_classifier/test_threshold.py
@@ -72,7 +72,7 @@ def test_binary_discrimination_threshold(self):
         visualizer = DiscriminationThreshold(model, ax=ax, random_state=23)
 
         visualizer.fit(X, y)
-        visualizer.poof()
+        visualizer.finalize()
 
         self.assert_images_similar(visualizer)
 
@@ -119,7 +119,7 @@ def test_pandas_integration(self):
             LogisticRegression(), ax=ax, classes=classes, random_state=193
         )
         viz.fit(X, y)
-        viz.poof()
+        viz.finalize()
 
         self.assert_images_similar(viz, tol=0.1)
 
@@ -184,7 +184,7 @@ def test_binary_discrimination_threshold_alt_args(self):
         )
 
         visualizer.fit(X, y)
-        visualizer.poof()
+        visualizer.finalize()
 
         for metric in exclude:
             assert metric not in visualizer.cv_scores_
diff --git a/tests/test_cluster/test_base.py b/tests/test_cluster/test_base.py
index 66d91ada8..41aa4c1bd 100644
--- a/tests/test_cluster/test_base.py
+++ b/tests/test_cluster/test_base.py
@@ -17,7 +17,7 @@
 ## Imports
 ##########################################################################
 
-import unittest
+import pytest
 
 from yellowbrick.exceptions import YellowbrickTypeError
 from yellowbrick.cluster.base import ClusteringScoreVisualizer
@@ -28,30 +28,31 @@
 from sklearn.cluster import KMeans, MiniBatchKMeans, AffinityPropagation
 from sklearn.cluster import MeanShift, DBSCAN, Birch
 
+
 ##########################################################################
 ## Clustering Base Test Cases
 ##########################################################################
 
-class ClusterBaseTests(unittest.TestCase):
+class TestClusterBase(object):
+
+    @pytest.mark.parametrize("model", [
+        SVC, SVR, Ridge, RidgeCV, LinearRegression, RandomForestClassifier
+    ])
+    def test_clusterer_enforcement_raises(self, model):
+        """
+        Assert that non-cluster models raise a TypeError for cluster visualizers
+        """
+        with pytest.raises(YellowbrickTypeError):
+            ClusteringScoreVisualizer(model())
 
-    def test_clusterer_enforcement(self):
+    @pytest.mark.parametrize("model", [
+        KMeans, MiniBatchKMeans, AffinityPropagation, MeanShift, DBSCAN, Birch
+    ])
+    def test_clusterer_enforcement(self, model):
         """
         Assert that only clustering estimators can be passed to cluster viz
         """
-        nomodels = [
-            SVC, SVR, Ridge, RidgeCV, LinearRegression, RandomForestClassifier
-        ]
-
-        for nomodel in nomodels:
-            with self.assertRaises(YellowbrickTypeError):
-                ClusteringScoreVisualizer(nomodel())
-
-        models = [
-            KMeans, MiniBatchKMeans, AffinityPropagation, MeanShift, DBSCAN, Birch
-        ]
-
-        for model in models:
-            try:
-                ClusteringScoreVisualizer(model())
-            except YellowbrickTypeError:
-                self.fail("could not pass clustering estimator to visualizer")
+        try:
+            ClusteringScoreVisualizer(model())
+        except YellowbrickTypeError:
+            self.fail("could not pass clustering estimator to visualizer")
diff --git a/tests/test_cluster/test_elbow.py b/tests/test_cluster/test_elbow.py
index 5cfcf0d8a..77cf72c0b 100644
--- a/tests/test_cluster/test_elbow.py
+++ b/tests/test_cluster/test_elbow.py
@@ -143,7 +143,7 @@ def test_integrated_kmeans_elbow(self):
 
             visualizer = KElbowVisualizer(KMeans(random_state=42), k=4, ax=ax)
             visualizer.fit(X)
-            visualizer.poof()
+            visualizer.finalize()
 
             self.assert_images_similar(visualizer)
         except Exception as e:
@@ -157,7 +157,7 @@ def test_integrated_mini_batch_kmeans_elbow(self):
         # NOTE #182: cannot use occupancy dataset because of memory usage
 
         # Generate a blobs data set
-        X,y = make_blobs(
+        X, y = make_blobs(
             n_samples=1000, n_features=12, centers=6, shuffle=True, random_state=42
         )
 
@@ -168,7 +168,7 @@ def test_integrated_mini_batch_kmeans_elbow(self):
                 MiniBatchKMeans(random_state=42), k=4, ax=ax
             )
             visualizer.fit(X)
-            visualizer.poof()
+            visualizer.finalize()
 
             self.assert_images_similar(visualizer)
         except Exception as e:
@@ -186,7 +186,7 @@ def test_topic_modeling_k_means(self):
         visualizer = KElbowVisualizer(KMeans(), k=(4, 8))
 
         visualizer.fit(docs)
-        visualizer.poof()
+        visualizer.finalize()
 
         self.assert_images_similar(visualizer)
 
@@ -236,7 +236,7 @@ def test_distortion_metric(self):
         expected = np.array([ 69.100065, 54.081571, 43.146921, 34.978487])
         assert len(visualizer.k_scores_) == 4
 
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer)
         assert_array_almost_equal(visualizer.k_scores_, expected)
 
@@ -255,7 +255,7 @@ def test_silhouette_metric(self):
         expected = np.array([ 0.691636,  0.456646,  0.255174,  0.239842])
         assert len(visualizer.k_scores_) == 4
 
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer)
         assert_array_almost_equal(visualizer.k_scores_, expected)
 
@@ -279,13 +279,13 @@ def test_calinski_harabasz_metric(self):
             40.952179227847012, 35.939494
         ])
 
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer)
         assert_array_almost_equal(visualizer.k_scores_, expected)
 
     def test_locate_elbow(self):
         """
-        Test the addition of locate_elbow to an image 
+        Test the addition of locate_elbow to an image
         """
         X,y = make_blobs(
             n_samples=1000, n_features=5, centers=3, shuffle=True, random_state=42
@@ -303,7 +303,7 @@ def test_locate_elbow(self):
             4286.479848, 12463.383743, 8766.999551, 6950.08391, 5865.79722
         ])
 
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, windows_tol=2.2)
         assert_array_almost_equal(visualizer.k_scores_, expected)
 
@@ -347,6 +347,6 @@ def test_timings(self):
 
         # call draw again which is normally called in fit
         visualizer.draw()
-        visualizer.poof()
+        visualizer.finalize()
 
         self.assert_images_similar(visualizer)
diff --git a/tests/test_cluster/test_silhouette.py b/tests/test_cluster/test_silhouette.py
index 9538ec280..bdbd9b47d 100644
--- a/tests/test_cluster/test_silhouette.py
+++ b/tests/test_cluster/test_silhouette.py
@@ -34,9 +34,9 @@
 ## SilhouetteVisualizer Test Cases
 ##########################################################################
 
-class SilhouetteVisualizerTests(VisualTestCase):
+class TestSilhouetteVisualizer(VisualTestCase):
     """
-    Silhouette Visualizer
+    Silhouette Visualizer Tests
     """
 
     @pytest.mark.xfail(
@@ -59,7 +59,7 @@ def test_integrated_kmeans_silhouette(self):
 
             visualizer = SilhouetteVisualizer(KMeans(random_state=0), ax=ax)
             visualizer.fit(X)
-            visualizer.poof()
+            visualizer.finalize()
 
             self.assert_images_similar(visualizer, remove_legend=True)
         except Exception as e:
@@ -85,7 +85,7 @@ def test_integrated_mini_batch_kmeans_silhouette(self):
 
             visualizer = SilhouetteVisualizer(MiniBatchKMeans(random_state=0), ax=ax)
             visualizer.fit(X)
-            visualizer.poof()
+            visualizer.finalize()
 
             self.assert_images_similar(visualizer, remove_legend=True)
         except Exception as e:
@@ -118,7 +118,7 @@ def test_colormap_silhouette(self):
 
             visualizer = SilhouetteVisualizer(MiniBatchKMeans(random_state=0), ax=ax, colormap='gnuplot')
             visualizer.fit(X)
-            visualizer.poof()
+            visualizer.finalize()
 
             self.assert_images_similar(visualizer, remove_legend=True)
         except Exception as e:
@@ -145,7 +145,7 @@ def test_colors_silhouette(self):
                 colors=['red', 'green', 'blue', 'indigo', 'cyan', 'lavender']
             )
             visualizer.fit(X)
-            visualizer.poof()
+            visualizer.finalize()
 
             self.assert_images_similar(visualizer, remove_legend=True)
         except Exception as e:
@@ -167,7 +167,7 @@ def test_colormap_as_colors_silhouette(self):
 
             visualizer = SilhouetteVisualizer(MiniBatchKMeans(random_state=0), ax=ax, colors='cool')
             visualizer.fit(X)
-            visualizer.poof()
+            visualizer.finalize()
 
             tol = 3.2 if sys.platform == "win32" else 0.01  # Fails on AppVeyor with RMS 3.143
             self.assert_images_similar(visualizer, remove_legend=True, tol=tol)
diff --git a/tests/test_contrib/test_classifier/test_boundaries.py b/tests/test_contrib/test_classifier/test_boundaries.py
index 301fc3a78..e2fa24256 100644
--- a/tests/test_contrib/test_classifier/test_boundaries.py
+++ b/tests/test_contrib/test_classifier/test_boundaries.py
@@ -66,7 +66,7 @@
 
 
 @pytest.mark.filterwarnings('ignore')
-class DecisionBoundariesVisualizerTest(VisualTestCase):
+class TestDecisionBoundariesVisualizer(VisualTestCase):
     """
     Test DecisionBoundariesVisualizer
     """
@@ -102,24 +102,23 @@ def test_init(self):
         model = neighbors.KNeighborsClassifier(3)
         viz = DecisionBoundariesVisualizer(model)
 
-        self.assertEquals(viz.step_size, 0.0025)
-        self.assertEqual(viz.name, 'KNeighborsClassifier')
-        self.assertEqual(viz.estimator, model)
+        assert viz.step_size == 0.0025
+        assert viz.name == 'KNeighborsClassifier'
+        assert viz.estimator is model
 
-        self.assertIsNone(viz.classes_)
-        self.assertIsNone(viz.features_)
-        self.assertIsNotNone(viz.markers)
-        self.assertIsNotNone(viz.scatter_alpha)
-        self.assertTrue(viz.show_scatter)
-
-        self.assertIsNone(viz.Z)
-        self.assertIsNone(viz.xx)
-        self.assertIsNone(viz.yy)
-        self.assertIsNone(viz.class_labels)
-        self.assertIsNone(viz.title)
-        self.assertIsNone(viz.x)
-        self.assertIsNone(viz.y)
+        assert viz.classes_ is None
+        assert viz.features_ is None
+        assert viz.markers is not None
+        assert viz.scatter_alpha is not None
+        assert viz.show_scatter is True
 
+        assert viz.Z is None
+        assert viz.xx is None
+        assert viz.yy is None
+        assert viz.class_labels is None
+        assert viz.title is None
+        assert viz.x is None
+        assert viz.y is None
 
     def test_scatter_xy_and_features_raise_error(self):
         """
@@ -128,7 +127,7 @@ def test_scatter_xy_and_features_raise_error(self):
         model = neighbors.KNeighborsClassifier(3)
         features = ["temperature", "relative_humidity", "light"]
 
-        with self.assertRaises(YellowbrickValueError):
+        with pytest.raises(YellowbrickValueError):
             DecisionBoundariesVisualizer(
                 model, features=features, x='one', y='two'
             )
@@ -139,8 +138,7 @@ def test_scatter_xy_changes_to_features(self):
         """
         model = neighbors.KNeighborsClassifier(3)
         visualizer = DecisionBoundariesVisualizer(model, x='one', y='two')
-        self.assertEquals(visualizer.features_, ['one', 'two'])
-
+        assert visualizer.features_ == ['one', 'two']
 
     def test_fit(self):
         """
@@ -154,17 +152,17 @@ def test_fit(self):
         fitted_viz = viz.fit(X_two_cols, y=y)
 
         # assert that classes and labels are established
-        self.assertEqual(fitted_viz.classes_, {0: '0', 1: '1', 2: '2', 3: '3'})
-        self.assertEqual(fitted_viz.features_, ['Feature One', 'Feature Two'])
+        assert fitted_viz.classes_ == {0: '0', 1: '1', 2: '2', 3: '3'}
+        assert fitted_viz.features_ == ['Feature One', 'Feature Two']
 
         # assert that the fit method is called
         model.fit.assert_called_once_with(X_two_cols, y)
         # mock object is called twice in predict and reshape
-        self.assertEqual(len(model.predict.mock_calls), 2)
+        assert len(model.predict.mock_calls) == 2
 
         # test that attrs are set
-        self.assertIsNotNone(fitted_viz.ax)
-        self.assertIsNotNone(fitted_viz.Z_shape)
+        assert fitted_viz.ax is not None
+        assert fitted_viz.Z_shape is not None
 
     def test_fit_class_labels(self):
         """
@@ -174,11 +172,7 @@ def test_fit_class_labels(self):
         viz = DecisionBoundariesVisualizer(
             model, classes=['one', 'two', 'three', 'four'])
         fitted_viz = viz.fit(X_two_cols, y=y)
-        self.assertEquals(fitted_viz.classes_,
-                          {'three': '2',
-                           'four': '3',
-                           'two': '1',
-                           'one': '0'})
+        assert fitted_viz.classes_ == {'three': '2', 'four': '3', 'two': '1', 'one': '0'}
 
     def test_fit_class_labels_class_names_edge_case(self):
         """
@@ -187,7 +181,9 @@ def test_fit_class_labels_class_names_edge_case(self):
         model = neighbors.KNeighborsClassifier(3)
         viz = DecisionBoundariesVisualizer(
             model, classes=['one', 'two', 'three', 'four', 'five'])
-        self.assertRaises(YellowbrickTypeError, viz.fit, X_two_cols, y=y)
+
+        with pytest.raises(YellowbrickTypeError):
+            viz.fit(X_two_cols, y=y)
 
     def test_fit_features_assignment_None(self):
         """
@@ -195,9 +191,9 @@ def test_fit_features_assignment_None(self):
         """
         model = neighbors.KNeighborsClassifier(3)
         viz = DecisionBoundariesVisualizer(model)
-        self.assertIsNone(viz.features_)
+        assert viz.features_ is None
         fitted_viz = viz.fit(X_two_cols, y=y)
-        self.assertEquals(fitted_viz.features_, ['Feature One', 'Feature Two'])
+        assert fitted_viz.features_ == ['Feature One', 'Feature Two']
 
     def test_fit_features_assignment(self):
         """
@@ -206,7 +202,7 @@ def test_fit_features_assignment(self):
         model = neighbors.KNeighborsClassifier(3)
         viz = DecisionBoundariesVisualizer(model, features=['one', 'two'])
         fitted_viz = viz.fit(X_two_cols, y=y)
-        self.assertEquals(fitted_viz.features_, ['one', 'two'])
+        assert fitted_viz.features_ == ['one', 'two']
 
     @mock.patch("yellowbrick.contrib.classifier.boundaries.OrderedDict")
     def test_draw_ordereddict_calls(self, mock_odict):
@@ -216,8 +212,11 @@ def test_draw_ordereddict_calls(self, mock_odict):
         mock_odict.return_value = {}
         model = neighbors.KNeighborsClassifier(3)
         viz = DecisionBoundariesVisualizer(model, features=['one', 'two'])
-        self.assertRaises(KeyError, viz.fit_draw, X_two_cols, y=y)
-        self.assertEquals(len(mock_odict.mock_calls), 2)
+
+        with pytest.raises(KeyError):
+            viz.fit_draw(X_two_cols, y=y)
+
+        assert len(mock_odict.mock_calls) == 2
 
     @mock.patch("yellowbrick.contrib.classifier.boundaries.resolve_colors")
     def test_draw_ordereddict_calls_one(self, mock_resolve_colors):
@@ -227,8 +226,11 @@ def test_draw_ordereddict_calls_one(self, mock_resolve_colors):
         mock_resolve_colors.return_value = []
         model = neighbors.KNeighborsClassifier(3)
         viz = DecisionBoundariesVisualizer(model, features=['one', 'two'])
-        self.assertRaises(StopIteration, viz.fit_draw, X_two_cols, y=y)
-        self.assertEquals(len(mock_resolve_colors.mock_calls), 1)
+
+        with pytest.raises(StopIteration):
+            viz.fit_draw(X_two_cols, y=y)
+
+        assert len(mock_resolve_colors.mock_calls) == 1
 
     def test_draw_ax_show_scatter_true(self):
         """
@@ -243,9 +245,9 @@ def test_draw_ax_show_scatter_true(self):
         fitted_viz.ax.legend = mock.MagicMock()
 
         fitted_viz.draw(X_two_cols, y=y)
-        self.assertEquals(len(fitted_viz.ax.pcolormesh.mock_calls), 1)
-        self.assertEquals(len(fitted_viz.ax.scatter.mock_calls), 4)
-        self.assertEquals(len(fitted_viz.ax.legend.mock_calls), 0)
+        assert len(fitted_viz.ax.pcolormesh.mock_calls) == 1
+        assert len(fitted_viz.ax.scatter.mock_calls) == 4
+        assert len(fitted_viz.ax.legend.mock_calls) == 0
 
     def test_draw_ax_show_scatter_False(self):
         """
@@ -261,9 +263,9 @@ def test_draw_ax_show_scatter_False(self):
         fitted_viz.ax.legend = mock.MagicMock()
 
         fitted_viz.draw(X_two_cols, y=y)
-        self.assertEquals(len(fitted_viz.ax.pcolormesh.mock_calls), 1)
-        self.assertEquals(len(fitted_viz.ax.scatter.mock_calls), 0)
-        self.assertEquals(len(fitted_viz.ax.legend.mock_calls), 1)
+        assert len(fitted_viz.ax.pcolormesh.mock_calls) == 1
+        assert len(fitted_viz.ax.scatter.mock_calls) == 0
+        assert len(fitted_viz.ax.legend.mock_calls) == 1
 
     def test_finalize(self):
         """
@@ -280,7 +282,7 @@ def test_finalize(self):
         fitted_viz.ax.set_xlabel = mock.MagicMock()
         fitted_viz.ax.set_ylabel = mock.MagicMock()
 
-        fitted_viz.poof()
+        fitted_viz.finalize()
 
         fitted_viz.ax.legend.assert_called_once_with(loc='best', frameon=True)
         fitted_viz.ax.set_xlabel.assert_called_once_with('one')
@@ -345,7 +347,7 @@ def test_integrated_plot_numpy_named_arrays(self):
 
         visualizer = DecisionBoundariesVisualizer(model, features=['a', 'f'])
         visualizer.fit_draw_poof(X, y=y)
-        self.assertEquals(visualizer.features_, ['a', 'f'])
+        assert visualizer.features_ == ['a', 'f']
         self.assert_images_similar(visualizer)
 
     def test_integrated_scatter_numpy_arrays_no_names(self):
@@ -356,7 +358,7 @@ def test_integrated_scatter_numpy_arrays_no_names(self):
 
         visualizer = DecisionBoundariesVisualizer(model, features=[1, 2])
         visualizer.fit_draw_poof(X, y)
-        self.assertEquals(visualizer.features_, [1, 2])
+        assert visualizer.features_ == [1, 2]
 
     @pytest.mark.xfail(
         sys.platform == 'win32', reason="images not close on windows"
diff --git a/tests/test_contrib/test_missing/test_bar.py b/tests/test_contrib/test_missing/test_bar.py
index 0ab1ee3d4..5c92a02a1 100644
--- a/tests/test_contrib/test_missing/test_bar.py
+++ b/tests/test_contrib/test_missing/test_bar.py
@@ -18,6 +18,9 @@
 ##########################################################################
 
 import os
+import pytest
+import numpy as np
+
 from tests.base import VisualTestCase
 from sklearn.datasets import make_classification
 from yellowbrick.contrib.missing.bar import *
@@ -27,21 +30,22 @@
 except ImportError:
     pd = None
 
+
+@pytest.fixture(scope="class")
+def missing_bar_tolerance(request):
+    request.cls.tol = 0.5 if os.name == 'nt' else 0.01
+
+
 ##########################################################################
 ## Feature Importances Tests
 ##########################################################################
 
+@pytest.mark.usefixtures("missing_bar_tolerance")
 class TestMissingBarVisualizer(VisualTestCase):
     """
     FeatureImportances visualizer
     """
 
-    def setUp(self):
-        super(TestMissingBarVisualizer, self).setUp()
-        self.tol = 0.01
-        if os.name == 'nt': # Windows
-            self.tol = 0.5
-
     def test_missingvaluesbar_pandas(self):
         """
         Integration test of visualizer with pandas
@@ -58,11 +62,10 @@ def test_missingvaluesbar_pandas(self):
         features = [str(n) for n in range(20)]
         viz = MissingValuesBar(features=features)
         viz.fit(X_)
-        viz.poof()
+        viz.finalize()
 
         self.assert_images_similar(viz, tol=self.tol)
 
-
     def test_missingvaluesbar_numpy(self):
         """
         Integration test of visualizer with numpy without target y passed in
@@ -78,7 +81,7 @@ def test_missingvaluesbar_numpy(self):
         features = [str(n) for n in range(20)]
         viz = MissingValuesBar(features=features)
         viz.fit(X)
-        viz.poof()
+        viz.finalize()
 
         self.assert_images_similar(viz, tol=self.tol)
 
@@ -98,7 +101,7 @@ def test_missingvaluesbar_numpy_with_y_target(self):
         features = [str(n) for n in range(20)]
         viz = MissingValuesBar(features=features)
         viz.fit(X, y)
-        viz.poof()
+        viz.finalize()
 
         self.assert_images_similar(viz, tol=self.tol)
 
@@ -118,6 +121,6 @@ def test_missingvaluesbar_numpy_with_y_target_with_labels(self):
         features = [str(n) for n in range(20)]
         viz = MissingValuesBar(features=features, classes=['class A', 'class B'])
         viz.fit(X, y)
-        viz.poof()
+        viz.finalize()
 
         self.assert_images_similar(viz, tol=self.tol)
diff --git a/tests/test_contrib/test_missing/test_dispersion.py b/tests/test_contrib/test_missing/test_dispersion.py
index 0636f7a70..7735c67e8 100644
--- a/tests/test_contrib/test_missing/test_dispersion.py
+++ b/tests/test_contrib/test_missing/test_dispersion.py
@@ -16,7 +16,10 @@
 ##########################################################################
 ## Imports
 ##########################################################################
+
 import os
+import pytest
+
 from sklearn.datasets import make_classification
 from tests.base import VisualTestCase
 
@@ -27,20 +30,21 @@
 except ImportError:
     pd = None
 
+
+@pytest.fixture(scope="class")
+def missing_dispersion_tolerance(request):
+    request.cls.tol = 0.5 if os.name == 'nt' else 0.01
+
+
 ##########################################################################
 ## Feature Importances Tests
 ##########################################################################
 
-class MissingValuesDispersionTestCase(VisualTestCase):
+@pytest.mark.usefixtures("missing_dispersion_tolerance")
+class TestMissingValuesDispersion(VisualTestCase):
     """
     MissingValuesDispersion visualizer
     """
-    def setUp(self):
-        super(MissingValuesDispersionTestCase, self).setUp()
-        self.tol = 0.01
-        if os.name == 'nt': # Windows
-            self.tol = 5.0
-
 
     def test_missingvaluesdispersion_with_pandas(self):
         """
@@ -58,7 +62,7 @@ def test_missingvaluesdispersion_with_pandas(self):
         features = [str(n) for n in range(20)]
         viz = MissingValuesDispersion(features=features)
         viz.fit(X_)
-        viz.poof()
+        viz.finalize()
 
         self.assert_images_similar(viz, tol=self.tol)
 
@@ -79,11 +83,10 @@ def test_missingvaluesdispersion_with_pandas_with_y_targets(self):
         classes = ['Class A', 'Class B']
         viz = MissingValuesDispersion(features=features, classes=classes)
         viz.fit(X_, y=y)
-        viz.poof()
+        viz.finalize()
 
         self.assert_images_similar(viz, tol=self.tol)
 
-
     def test_missingvaluesdispersion_with_numpy(self):
         """
         Integration test of visualizer with numpy
@@ -99,7 +102,7 @@ def test_missingvaluesdispersion_with_numpy(self):
         features = [str(n) for n in range(20)]
         viz = MissingValuesDispersion(features=features)
         viz.fit(X)
-        viz.poof()
+        viz.finalize()
 
         self.assert_images_similar(viz, tol=self.tol)
 
@@ -119,6 +122,6 @@ def test_missingvaluesdispersion_with_numpy_with_y_targets(self):
         classes = ['Class A', 'Class B']
         viz = MissingValuesDispersion(features=features, classes=classes)
         viz.fit(X, y=y)
-        viz.poof()
+        viz.finalize()
 
         self.assert_images_similar(viz, tol=self.tol)
diff --git a/tests/test_contrib/test_scatter.py b/tests/test_contrib/test_scatter.py
index 86f6d657a..1d6bb9bd6 100644
--- a/tests/test_contrib/test_scatter.py
+++ b/tests/test_contrib/test_scatter.py
@@ -16,11 +16,10 @@
 # Imports
 ##########################################################################
 
-from unittest import mock
-
-import matplotlib as mpl
 import pytest
+import matplotlib as mpl
 
+from unittest import mock
 from tests.base import VisualTestCase
 from yellowbrick.contrib.scatter import *
 from yellowbrick.datasets import load_occupancy
@@ -38,7 +37,7 @@
 ##########################################################################
 
 @pytest.mark.filterwarnings('ignore')
-class ScatterVizTests(VisualTestCase):
+class TestScatterViz(VisualTestCase):
     """
     Test ScatterViz
     """
@@ -61,7 +60,7 @@ def test_init_alias(self):
         """
         features = ["temperature", "relative humidity"]
         visualizer = ScatterVisualizer(features=features, markers=['*'])
-        self.assertIsNotNone(visualizer.markers)
+        assert visualizer.markers is not None
 
     def test_scatter(self):
         """
@@ -89,7 +88,7 @@ def test_scatter_no_features(self):
         X_two_cols = self.X[:, :2]
         visualizer = ScatterViz()
         visualizer.fit_transform_poof(X_two_cols, self.y)
-        self.assertEquals(visualizer.features_, ['Feature One', 'Feature Two'])
+        assert visualizer.features_ == ['Feature One', 'Feature Two']
 
     def test_scatter_only_two_features_allowed_init(self):
         """
@@ -97,7 +96,7 @@ def test_scatter_only_two_features_allowed_init(self):
         """
         features = ["temperature", "relative humidity", "light"]
 
-        with self.assertRaises(YellowbrickValueError):
+        with pytest.raises(YellowbrickValueError):
             ScatterViz(features=features)
 
     def test_scatter_xy_and_features_raise_error(self):
@@ -106,7 +105,7 @@ def test_scatter_xy_and_features_raise_error(self):
         """
         features = ["temperature", "relative humidity", "light"]
 
-        with self.assertRaises(YellowbrickValueError):
+        with pytest.raises(YellowbrickValueError):
             ScatterViz(features=features, x='one', y='two')
 
     def test_scatter_xy_changes_to_features(self):
@@ -114,17 +113,15 @@ def test_scatter_xy_changes_to_features(self):
         Assert that x,y with no features will not raise scatterviz error
         """
         visualizer = ScatterViz(x='one', y='two')
-        self.assertEquals(visualizer.features_, ['one', 'two'])
+        assert visualizer.features_ == ['one', 'two']
 
     def test_scatter_requires_two_features_in_numpy_matrix(self):
         """
         Assert only two features allowed for scatter visualizer if not in init
         """
         visualizer = ScatterViz()
-        with self.assertRaises(YellowbrickValueError) as context:
+        with pytest.raises(YellowbrickValueError, match='only accepts two features'):
             visualizer.fit_transform(self.X, self.y)
-            self.assertTrue(
-                'only accepts two features' in str(context.exception))
 
     def test_integrated_scatter(self):
         """
@@ -170,7 +167,7 @@ def test_scatter_quick_method(self):
         ax = scatterviz(X[:, :2], y=y, ax=None, features=features)
 
         # test that is returns a matplotlib obj with axes
-        self.assertIsInstance(ax, mpl.axes.Axes)
+        assert isinstance(ax, mpl.axes.Axes)
 
     @pytest.mark.skipif(pd is None, reason="pandas is required for this test")
     def test_integrated_scatter_with_pandas(self):
@@ -205,7 +202,7 @@ def test_integrated_scatter_numpy_named_arrays(self):
         X_named = self.X.astype(dt, casting='unsafe')
         visualizer = ScatterViz(features=['one', 'two'])
         visualizer.fit_transform_poof(X_named, self.y)
-        self.assertEquals(visualizer.features_, ['one', 'two'])
+        assert visualizer.features_ == ['one', 'two']
 
     def test_integrated_scatter_numpy_arrays_no_names(self):
         """
@@ -213,7 +210,7 @@ def test_integrated_scatter_numpy_arrays_no_names(self):
         """
         visualizer = ScatterViz(features=[1, 2])
         visualizer.fit_transform_poof(self.X, self.y)
-        self.assertEquals(visualizer.features_, [1, 2])
+        assert visualizer.features_ == [1, 2]
 
     def test_scatter_image(self):
         """
diff --git a/tests/test_draw.py b/tests/test_draw.py
index a7a111961..fc740a059 100644
--- a/tests/test_draw.py
+++ b/tests/test_draw.py
@@ -36,14 +36,14 @@ def test_manual_legend_uneven_colors():
 
 @pytest.fixture(scope="class")
 def data(request):
-    
+
     data = np.array(
             [[4, 8, 7, 6, 5, 2, 1],
              [6, 7, 9, 6, 9, 3, 6],
              [5, 1, 6, 8, 4, 7, 8],
              [6, 8, 1, 5, 6, 7, 4]]
             )
-    
+
     request.cls.data = data
 
 ##########################################################################
@@ -83,80 +83,79 @@ def test_manual_legend(self):
 
     def test_vertical_bar_stack(self):
         """
-        Test bar_stack for vertical orientation 
+        Test bar_stack for vertical orientation
         """
         _, ax = plt.subplots()
-        
+
         # Plots stacked bar charts
         bar_stack(self.data, ax=ax, orientation='v')
-        
+
          # Assert image similarity
         self.assert_images_similar(ax=ax, tol=0.1)
-        
+
     def test_horizontal_bar_stack(self):
         """
-        Test bar_stack for horizontal orientation 
+        Test bar_stack for horizontal orientation
         """
         _, ax = plt.subplots()
         # Plots stacked bar charts
         bar_stack(self.data, ax=ax, orientation='h')
-        
+
         # Assert image similarity
         self.assert_images_similar(ax=ax, tol=0.1)
-        
+
     def test_single_row_bar_stack(self):
         """
-        Test bar_stack for single row 
-        """        
+        Test bar_stack for single row
+        """
         data = np.array([[4, 8, 7, 6, 5, 2, 1]])
-        
+
         _, ax = plt.subplots()
-        
+
         # Plots stacked bar charts
         bar_stack(data, ax=ax)
-        
+
         # Assert image similarity
         self.assert_images_similar(ax=ax, tol=0.1)
-    
+
     def test_labels_vertical(self):
         """
         Test labels and ticks for vertical barcharts
-        """        
+        """
         labels =  ['books', 'cinema', 'cooking', 'gaming']
-        ticks = ['noun', 'verb', 'adverb', 'pronoun', 'preposition', 
+        ticks = ['noun', 'verb', 'adverb', 'pronoun', 'preposition',
                  'digit', 'other']
         _, ax = plt.subplots()
-        
+
         # Plots stacked bar charts
-        bar_stack(self.data, labels = labels, ticks=ticks, 
+        bar_stack(self.data, labels = labels, ticks=ticks,
                   colors=['r','b','g','y'])
-        
+
         # Extract tick labels from the plot
         ticks_ax = [tick.get_text() for tick in ax.xaxis.get_ticklabels()]
         #Assert that ticks are set properly
         assert ticks_ax==ticks
-        
+
         # Assert image similarity
         self.assert_images_similar(ax=ax, tol=0.05)
-    
+
     def test_labels_horizontal(self):
         """
         Test labels and ticks with horizontal barcharts
-        """        
+        """
         labels =  ['books', 'cinema', 'cooking', 'gaming']
-        ticks = ['noun', 'verb', 'adverb', 'pronoun', 'preposition', 
+        ticks = ['noun', 'verb', 'adverb', 'pronoun', 'preposition',
                  'digit', 'other']
         _, ax = plt.subplots()
-        
+
         # Plots stacked bar charts
-        bar_stack(self.data, labels = labels, ticks=ticks, orientation='h', 
+        bar_stack(self.data, labels = labels, ticks=ticks, orientation='h',
                   colormap='cool')
-        
+
         # Extract tick labels from the plot
         ticks_ax = [tick.get_text() for tick in ax.yaxis.get_ticklabels()]
         #Assert that ticks are set properly
         assert ticks_ax==ticks
-        
+
         # Assert image similarity
         self.assert_images_similar(ax=ax, tol=0.05)
-        
\ No newline at end of file
diff --git a/tests/test_features/test_base.py b/tests/test_features/test_base.py
index 086eaab9d..040551418 100644
--- a/tests/test_features/test_base.py
+++ b/tests/test_features/test_base.py
@@ -28,22 +28,13 @@
 ## FeatureVisualizer Base Tests
 ##########################################################################
 
-class FeatureVisualizerBaseTests(VisualTestCase):
+class TestFeatureVisualizerBase(VisualTestCase):
 
     def test_subclass(self):
         """
         Assert the feature visualizer is in its rightful place
         """
         visualizer = FeatureVisualizer()
-        self.assertIsInstance(visualizer, TransformerMixin)
-        self.assertIsInstance(visualizer, BaseEstimator)
-        self.assertIsInstance(visualizer, Visualizer)
-
-    # def test_interface(self):
-    #     """
-    #     Test the feature visualizer interface
-    #     """
-    #
-    #     visualizer = FeatureVisualizer()
-    #     with self.assertRaises(NotImplementedError):
-    #         visualizer.poof()
+        assert isinstance(visualizer, TransformerMixin)
+        assert isinstance(visualizer, BaseEstimator)
+        assert isinstance(visualizer, Visualizer)
diff --git a/tests/test_features/test_jointplot.py b/tests/test_features/test_jointplot.py
index 84b6832fc..1d7f0ad31 100644
--- a/tests/test_features/test_jointplot.py
+++ b/tests/test_features/test_jointplot.py
@@ -23,11 +23,12 @@
 ##########################################################################
 
 import sys
+import pytest
+import numpy as np
+
 from functools import partial
 from unittest.mock import patch, MagicMock
 
-import numpy as np
-import pytest
 from sklearn.datasets import make_classification, make_regression
 
 from tests.base import IS_WINDOWS_OR_CONDA, VisualTestCase
@@ -46,6 +47,7 @@
 except ImportError:
     pd = None
 
+
 ##########################################################################
 ## Fixtures
 ##########################################################################
diff --git a/tests/test_features/test_manifold.py b/tests/test_features/test_manifold.py
index fbb02c849..83f671bae 100644
--- a/tests/test_features/test_manifold.py
+++ b/tests/test_features/test_manifold.py
@@ -44,60 +44,50 @@ class TestManifold(VisualTestCase):
     Test Manifold visualizer
     """
 
-    def test_manifold_construction(self):
+    @pytest.mark.parametrize("algorithm", [
+        "lle", "ltsa", "hessian", "modified", "isomap", "mds", "spectral", "tsne",
+    ])
+    def test_manifold_construction(self, algorithm):
         """
         Should be able to construct a manifold estimator from a string
         """
-        # TODO: parametrize this once unittest.TestCase dependency removed.
-        algorithms = [
-            "lle", "ltsa", "hessian", "modified",
-            "isomap", "mds", "spectral", "tsne",
-        ]
-
-        for algorithm in algorithms:
-            message = "case failed for {}".format(algorithm)
-            params = {
-                "n_neighbors": 18,
-                "random_state": 53,
-            }
-            oz = Manifold(manifold=algorithm, **params)
-            assert is_estimator(oz.manifold), message
-            assert oz.manifold.get_params()["n_components"] == 2, message
-
-            manifold_params = oz.manifold.get_params()
-            for param, value in params.items():
-                if param in manifold_params:
-                    assert value == manifold_params[param], message
-
-    def test_manifold_warning(self):
+        message = "case failed for {}".format(algorithm)
+        params = {
+            "n_neighbors": 18,
+            "random_state": 53,
+        }
+        oz = Manifold(manifold=algorithm, **params)
+        assert is_estimator(oz.manifold), message
+        assert oz.manifold.get_params()["n_components"] == 2, message
+
+        manifold_params = oz.manifold.get_params()
+        for param, value in params.items():
+            if param in manifold_params:
+                assert value == manifold_params[param], message
+
+    @pytest.mark.parametrize("algorithm", [
+        "lle", "ltsa", "hessian", "modified", "isomap", "spectral",
+    ])
+    def test_manifold_warning(self, algorithm):
         """
         Should raise a warning if n_neighbors not specified
         """
-        # TODO: parametrize this once unittest.TestCase dependency removed.
-        algorithms = [
-            "lle", "ltsa", "hessian", "modified", "isomap", "spectral",
-        ]
+        message = "case failed for {}".format(algorithm)
+        n_neighbors = 6 if algorithm == "hessian" else 5
 
-        for algorithm in algorithms:
-            message = "case failed for {}".format(algorithm)
-            n_neighbors = 6 if algorithm == "hessian" else 5
+        with pytest.warns(YellowbrickWarning):
+            oz = Manifold(manifold=algorithm)
+            assert oz.n_neighbors == n_neighbors, message
 
-            with pytest.warns(YellowbrickWarning):
-                oz = Manifold(manifold=algorithm)
-                assert oz.n_neighbors == n_neighbors, message
-
-    def test_manifold_no_warning(self):
+    @pytest.mark.parametrize("algorithm", ["mds", "tsne"])
+    def test_manifold_no_warning(self, algorithm):
         """
         Should not raise a warning if n_neighbors not specified
         """
-        # TODO: parametrize this once unittest.TestCase dependency removed.
-        algorithms = ["mds", "tsne"]
-
-        for algorithm in algorithms:
-            message = "case failed for {}".format(algorithm)
+        message = "case failed for {}".format(algorithm)
 
-            with pytest.warns(None) as record:
-                assert not record.list, message
+        with pytest.warns(None) as record:
+            assert not record.list, message
 
     def test_bad_manifold_exception(self):
         """
@@ -216,21 +206,16 @@ def test_manifold_pandas(self):
         self.assert_images_similar(oz, tol=35)
 
     @pytest.mark.filterwarnings("ignore:Conversion of the second argument")
-    def test_manifold_algorithm_fit(self):
+    @pytest.mark.parametrize("algorithm", [
+        "lle", "ltsa", "hessian", "modified", "isomap", "mds", "spectral", "tsne",
+    ])
+    def test_manifold_algorithm_fit(self, algorithm):
         """
         Test that all algorithms can be fitted correctly
         """
-        # TODO: parametrize this once unittest.TestCase dependency removed.
-        algorithms = [
-            "lle", "ltsa", "hessian", "modified",
-            "isomap", "mds", "spectral", "tsne",
-        ]
-
         X, y = make_s_curve(200, random_state=888)
-
-        for algorithm in algorithms:
-            oz = Manifold(manifold=algorithm, n_neighbors=10, random_state=223)
-            oz.fit(X, y)
+        oz = Manifold(manifold=algorithm, n_neighbors=10, random_state=223)
+        oz.fit(X, y)
 
     def test_determine_target_color_type(self):
         """
diff --git a/tests/test_features/test_pca.py b/tests/test_features/test_pca.py
index 0ed5da16b..9dcedaacb 100644
--- a/tests/test_features/test_pca.py
+++ b/tests/test_features/test_pca.py
@@ -56,7 +56,7 @@ def binary(request):
 ##########################################################################
 
 @pytest.mark.usefixtures("binary")
-class PCADecompositionTests(VisualTestCase):
+class TestPCADecomposition(VisualTestCase):
     """
     Test the PCADecomposition visualizer
     """
@@ -191,7 +191,7 @@ def test_scale_true_3d_execption(self):
         with pytest.raises(ValueError, match=e):
             pca = PCADecomposition(**params)
             pca.fit(X)
-    
+
     @mock.patch('yellowbrick.features.pca.plt.sca', autospec=True)
     def test_alpha_param(self, mock_sca):
         """
@@ -202,7 +202,7 @@ def test_alpha_param(self, mock_sca):
         visualizer = PCADecomposition(**params).fit(self.dataset.X)
         pca_array = visualizer.transform(self.dataset.X)
         assert visualizer.alpha == 0.3
-        
+
         visualizer.ax = mock.MagicMock()
         visualizer.fit(self.dataset.X)
         visualizer.transform(self.dataset.X)
@@ -212,4 +212,4 @@ def test_alpha_param(self, mock_sca):
         assert "alpha" in scatter_kwargs
         assert scatter_kwargs["alpha"] == 0.3
         assert pca_array.shape == (self.dataset.X.shape[0], 2)
-        
+
diff --git a/tests/test_features/test_pcoords.py b/tests/test_features/test_pcoords.py
index 1227040a6..ade68b43b 100644
--- a/tests/test_features/test_pcoords.py
+++ b/tests/test_features/test_pcoords.py
@@ -70,7 +70,7 @@ def test_parallel_coords(self):
         """
         visualizer = ParallelCoordinates()
         visualizer.fit_transform(self.dataset.X, self.dataset.y)
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=0.25)
 
     def test_parallel_coords_fast(self):
@@ -79,7 +79,7 @@ def test_parallel_coords_fast(self):
         """
         visualizer = ParallelCoordinates(fast=True)
         visualizer.fit_transform(self.dataset.X, self.dataset.y)
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=0.25)
 
     def test_alpha(self):
@@ -88,7 +88,7 @@ def test_alpha(self):
         """
         visualizer = ParallelCoordinates(alpha=1.0)
         visualizer.fit_transform(self.dataset.X, self.dataset.y)
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=0.25)
 
     def test_alpha_fast(self):
@@ -97,7 +97,7 @@ def test_alpha_fast(self):
         """
         visualizer = ParallelCoordinates(alpha=1.0, fast=True)
         visualizer.fit_transform(self.dataset.X, self.dataset.y)
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=0.25)
 
     def test_labels(self):
@@ -108,7 +108,7 @@ def test_labels(self):
             classes=['a', 'b', 'c'], features=['f1', 'f2', 'f3', 'f4', 'f5']
         )
         visualizer.fit_transform(self.dataset.X, self.dataset.y)
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer)
 
     def test_labels_fast(self):
@@ -119,7 +119,7 @@ def test_labels_fast(self):
             classes=['a', 'b', 'c'], features=['f1', 'f2', 'f3', 'f4', 'f5'], fast=True
         )
         visualizer.fit_transform(self.dataset.X, self.dataset.y)
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer)
 
     def test_normalized_l2(self):
@@ -128,7 +128,7 @@ def test_normalized_l2(self):
         """
         visualizer = ParallelCoordinates(normalize='l2')
         visualizer.fit_transform(self.dataset.X, self.dataset.y)
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=0.25)
 
     def test_normalized_l2_fast(self):
@@ -137,7 +137,7 @@ def test_normalized_l2_fast(self):
         """
         visualizer = ParallelCoordinates(normalize='l2', fast=True)
         visualizer.fit_transform(self.dataset.X, self.dataset.y)
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=0.25)
 
     def test_normalized_minmax(self):
@@ -146,7 +146,7 @@ def test_normalized_minmax(self):
         """
         visualizer = ParallelCoordinates(normalize='minmax')
         visualizer.fit_transform(self.dataset.X, self.dataset.y)
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=0.25)
 
     def test_normalized_minmax_fast(self):
@@ -155,7 +155,7 @@ def test_normalized_minmax_fast(self):
         """
         visualizer = ParallelCoordinates(normalize='minmax', fast=True)
         visualizer.fit_transform(self.dataset.X, self.dataset.y)
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=0.25)
 
     @pytest.mark.skipif(pd is None, reason="test requires pandas")
@@ -174,7 +174,7 @@ def test_pandas_integration_sampled(self):
             sample=0.05, shuffle=True, random_state=4291, classes=classes
         )
         oz.fit_transform(X, y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz, tol=0.1)
 
@@ -193,7 +193,7 @@ def test_numpy_integration_sampled(self):
             sample=0.05, shuffle=True, random_state=4291, classes=classes
         )
         oz.fit_transform(X, y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz, tol=0.1)
 
@@ -211,7 +211,7 @@ def test_pandas_integration_fast(self):
 
         oz = ParallelCoordinates(fast=True, classes=classes)
         oz.fit_transform(X, y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz, tol=0.1)
 
@@ -228,7 +228,7 @@ def test_numpy_integration_fast(self):
 
         oz = ParallelCoordinates(fast=True, classes=classes)
         oz.fit_transform(X, y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz, tol=0.1)
 
@@ -236,7 +236,7 @@ def test_normalized_invalid_arg(self):
         """
         Invalid argument to 'normalize' should raise
         """
-        with self.assertRaises(YellowbrickValueError):
+        with pytest.raises(YellowbrickValueError):
             ParallelCoordinates(normalize='foo')
 
     def test_sample_int(self):
@@ -276,7 +276,7 @@ def test_sample_int_invalid(self):
         """
         Negative int values should raise exception
         """
-        with self.assertRaises(YellowbrickValueError):
+        with pytest.raises(YellowbrickValueError):
             ParallelCoordinates(sample=-1)
 
     def test_sample_float(self):
@@ -316,16 +316,17 @@ def test_sample_float_invalid(self):
         """
         Float values for 'sample' argument outside [0,1] should raise.
         """
-        with self.assertRaises(YellowbrickValueError):
+        with pytest.raises(YellowbrickValueError):
             ParallelCoordinates(sample=-0.2)
-        with self.assertRaises(YellowbrickValueError):
+
+        with pytest.raises(YellowbrickValueError):
             ParallelCoordinates(sample=1.1)
 
     def test_sample_invalid_type(self):
         """
         Non-numeric values for 'sample' argument should raise.
         """
-        with self.assertRaises(YellowbrickTypeError):
+        with pytest.raises(YellowbrickTypeError):
             ParallelCoordinates(sample='foo')
 
     @staticmethod
diff --git a/tests/test_features/test_radviz.py b/tests/test_features/test_radviz.py
index fbda7d152..a7f40f3dc 100644
--- a/tests/test_features/test_radviz.py
+++ b/tests/test_features/test_radviz.py
@@ -96,7 +96,7 @@ def test_radviz(self):
         """
         visualizer = RadViz()
         visualizer.fit_transform(self.dataset.X, self.dataset.y)
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=0.25)
 
     def test_radviz_alpha(self):
@@ -105,7 +105,7 @@ def test_radviz_alpha(self):
         """
         visualizer = RadViz(alpha=0.5)
         visualizer.fit_transform(self.dataset.X, self.dataset.y)
-        visualizer.poof()
+        visualizer.finalize()
         self.assert_images_similar(visualizer, tol=0.25)
 
     @pytest.mark.xfail(
diff --git a/tests/test_features/test_rfecv.py b/tests/test_features/test_rfecv.py
index c428c5b18..d0fde420a 100644
--- a/tests/test_features/test_rfecv.py
+++ b/tests/test_features/test_rfecv.py
@@ -107,7 +107,7 @@ def test_rfecv_classification(self):
         cv = ShuffleSplit(3, random_state=21)
         oz = RFECV(SVC(kernel="linear", C=1), cv=cv)
         oz.fit(self.dataset.X, self.dataset.y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz, remove_legend=True)
 
@@ -144,7 +144,7 @@ def test_pandas_integration(self):
         cv = StratifiedKFold(n_splits=4, random_state=32)
         oz = RFECV(RandomForestClassifier(random_state=83), cv=cv)
         oz.fit(X, y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz, remove_legend=True)
 
@@ -164,17 +164,17 @@ def test_numpy_integration(self):
         cv = StratifiedKFold(n_splits=4, random_state=32)
         oz = RFECV(RandomForestClassifier(random_state=83), cv=cv)
         oz.fit(X, y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz, remove_legend=True)
 
-    def test_invalid_step(self):
+    @pytest.mark.parametrize("step", [0, -1, -5])
+    def test_invalid_step(self, step):
         """
         Test step hyperparam validation
         """
-        # TODO: parametrize when unittest is removed
         with pytest.raises(YellowbrickValueError, match="step must be >0"):
-            oz = RFECV(SVC(kernel="linear"), step=-1)
+            oz = RFECV(SVC(kernel="linear"), step=step)
             oz.fit(self.dataset.X, self.dataset.y)
 
     def test_rfecv_step(self):
diff --git a/tests/test_meta.py b/tests/test_meta.py
index 2d697d2c0..98a54e7de 100644
--- a/tests/test_meta.py
+++ b/tests/test_meta.py
@@ -113,7 +113,7 @@ def test_missing_baseline_image(self):
         Test that a missing basline image raises an exception
         """
         viz = RandomVisualizer(random_state=14).fit()
-        viz.poof()
+        viz.finalize()
 
         # Assert the baseline image does not exist
         assert_path_not_exists(
@@ -133,7 +133,7 @@ def test_random_visualizer(self):
         Test that a random visualization is correctly compared to a baseline
         """
         viz = RandomVisualizer(random_state=111).fit()
-        viz.poof()
+        viz.finalize()
 
         assert mpl.get_backend() == 'agg'
 
@@ -147,7 +147,7 @@ def test_random_visualizer_not_close(self):
         """
         # Baseline image random_state=225
         viz = RandomVisualizer(random_state=224).fit()
-        viz.poof()
+        viz.finalize()
 
         with pytest.raises(ImageComparisonFailure, match="images not close"):
             self.assert_images_similar(viz)
@@ -162,6 +162,6 @@ def test_random_visualizer_increased_tolerance(self):
         Test that not close visualizers pass with increased tolerance
         """
         viz = RandomVisualizer(random_state=224).fit()
-        viz.poof()
+        viz.finalize()
 
         self.assert_images_similar(viz, tol=30)
diff --git a/tests/test_model_selection/test_cross_validation.py b/tests/test_model_selection/test_cross_validation.py
index f6223c573..e3ea90d22 100644
--- a/tests/test_model_selection/test_cross_validation.py
+++ b/tests/test_model_selection/test_cross_validation.py
@@ -80,7 +80,7 @@ def test_classifier(self):
         )
 
         oz.fit(X, y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz, tol=2.0)
 
@@ -120,7 +120,7 @@ def test_regression(self):
         )
 
         oz.fit(X, y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz, tol=36.0)
 
@@ -178,6 +178,6 @@ def test_pandas_integration(self):
         oz = CVScores(BernoulliNB(), cv=cv)
 
         oz.fit(X, y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz, tol=2.0)
diff --git a/tests/test_model_selection/test_learning_curve.py b/tests/test_model_selection/test_learning_curve.py
index 06acccf73..f16a6467b 100644
--- a/tests/test_model_selection/test_learning_curve.py
+++ b/tests/test_model_selection/test_learning_curve.py
@@ -83,7 +83,7 @@ def test_classifier(self):
         oz = LearningCurve(
             RandomForestClassifier(random_state=21), random_state=12
         ).fit(X, y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz)
 
@@ -98,7 +98,7 @@ def test_regressor(self):
 
         oz = LearningCurve(Ridge(), random_state=18)
         oz.fit(X, y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz)
 
@@ -111,7 +111,7 @@ def test_clusters(self):
         oz = LearningCurve(
             MiniBatchKMeans(random_state=281), random_state=182
         ).fit(X)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz, tol=10)
 
@@ -153,7 +153,7 @@ def test_pandas_integration(self):
         cv = StratifiedKFold(n_splits=4, random_state=32)
         oz = LearningCurve(GaussianNB(), cv=cv, random_state=23)
         oz.fit(X, y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz)
 
diff --git a/tests/test_model_selection/test_validation_curve.py b/tests/test_model_selection/test_validation_curve.py
index 1dbacc741..f48fb2fa0 100644
--- a/tests/test_model_selection/test_validation_curve.py
+++ b/tests/test_model_selection/test_validation_curve.py
@@ -90,7 +90,7 @@ def test_classifier(self):
         )
 
         oz.fit(X, y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz)
 
@@ -109,7 +109,7 @@ def test_regression(self):
         )
 
         oz.fit(X, y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz, tol=12.0)
 
@@ -155,7 +155,7 @@ def test_pandas_integration(self):
             BernoulliNB(), cv=cv, param_range=pr, param_name='alpha'
         )
         oz.fit(X, y)
-        oz.poof()
+        oz.finalize()
 
         self.assert_images_similar(oz)
 
diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py
index d112148c2..aa582e8a9 100644
--- a/tests/test_pipeline.py
+++ b/tests/test_pipeline.py
@@ -18,7 +18,7 @@
 ##########################################################################
 
 import os
-import unittest
+import pytest
 
 from unittest import mock
 from yellowbrick.base import Visualizer
@@ -40,6 +40,7 @@ class MockEstimator(BaseEstimator):
     def fit(self, X, y=None, **kwargs):
         return self
 
+
 class MockVisualEstimator(Visualizer):
 
     def fit(self, X, y=None, **kwargs):
@@ -76,7 +77,7 @@ def draw(self, **kwargs):
 ## VisualPipeline Tests
 ##########################################################################
 
-class VisualPipelineTests(unittest.TestCase):
+class TestVisualPipeline(object):
 
     def test_validate_steps(self):
         """
@@ -87,7 +88,7 @@ def test_validate_steps(self):
         # TypeError if the steps don't match transforms --> estimator.
 
         # validate a bad intermediate transformer on the Pipeline
-        with self.assertRaises(TypeError):
+        with pytest.raises(TypeError):
             Pipeline([
                 ('real', MockTransformer()),
                 ('bad', Thing()),
@@ -95,7 +96,7 @@ def test_validate_steps(self):
             ])
 
         # validate a bad intermediate transformer on the VisualPipeline
-        with self.assertRaises(TypeError):
+        with pytest.raises(TypeError):
             VisualPipeline([
                 ('real', MockTransformer()),
                 ('bad', Thing()),
@@ -103,14 +104,14 @@ def test_validate_steps(self):
             ])
 
         # validate a bad final estimator on the Pipeline
-        with self.assertRaises(TypeError):
+        with pytest.raises(TypeError):
             Pipeline([
                 ('real', MockTransformer()),
                 ('bad', Thing()),
             ])
 
         # validate a bad final estimator on the VisualPipeline
-        with self.assertRaises(TypeError):
+        with pytest.raises(TypeError):
             VisualPipeline([
                 ('real', MockTransformer()),
                 ('bad', Thing()),
@@ -149,11 +150,11 @@ def test_visual_steps_property(self):
             ('e', MockEstimator()),
         ])
 
-        self.assertNotIn('a', pipeline.visual_steps)
-        self.assertIn('b', pipeline.visual_steps)
-        self.assertNotIn('c', pipeline.visual_steps)
-        self.assertIn('d', pipeline.visual_steps)
-        self.assertNotIn('e', pipeline.visual_steps)
+        assert 'a' not in pipeline.visual_steps
+        assert 'b' in pipeline.visual_steps
+        assert 'c' not in pipeline.visual_steps
+        assert 'd' in pipeline.visual_steps
+        assert 'e' not in pipeline.visual_steps
 
     def test_pipeline_poof(self):
         """
@@ -192,7 +193,7 @@ def test_pipeline_savefig_poof(self):
         pipeline.steps[3][1].poof.assert_called_once_with(outpath=os.path.join(tmpdir, "d.pdf"))
         pipeline.steps[4][1].poof.assert_called_once_with(outpath=os.path.join(tmpdir, "e.pdf"))
 
-    @unittest.skip("need to find a way for fit to return self in mocks")
+    @pytest.mark.skip(reason="need to find a way for fit to return self in mocks")
     def test_fit_transform_poof_and_draw_calls(self):
         """
         Test calling fit, transform, and poof on the pipeline
diff --git a/tests/test_regressor/test_alphas.py b/tests/test_regressor/test_alphas.py
index e7bf34608..c2d34dd0f 100644
--- a/tests/test_regressor/test_alphas.py
+++ b/tests/test_regressor/test_alphas.py
@@ -29,6 +29,8 @@
 from yellowbrick.exceptions import YellowbrickValueError
 
 from sklearn.svm import SVR, SVC
+from sklearn.cluster import KMeans
+from sklearn.decomposition import PCA
 from sklearn.datasets import make_regression
 from sklearn.linear_model import Ridge, RidgeCV
 from sklearn.linear_model import Lasso, LassoCV
@@ -57,33 +59,35 @@ def test_similar_image(self):
 
         X, y = make_regression(random_state=0)
         visualizer.fit(X, y)
-        visualizer.poof()
+        visualizer.finalize()
 
         self.assert_images_similar(visualizer)
 
-    def test_regressor_cv(self):
+    @pytest.mark.parametrize("model", [SVR, Ridge, Lasso, LassoLars, ElasticNet])
+    def test_regressor_nocv(self, model):
         """
         Ensure only "CV" regressors are allowed
         """
-        # TODO: parametrize with models when unittest dependency removed
-        for model in (SVR, Ridge, Lasso, LassoLars, ElasticNet):
-            with pytest.raises(YellowbrickTypeError):
-                AlphaSelection(model())
+        with pytest.raises(YellowbrickTypeError):
+            AlphaSelection(model())
 
-        # TODO: parametrize with models when unittest dependency removed (new test case)
-        for model in (RidgeCV, LassoCV, LassoLarsCV, ElasticNetCV):
-            try:
-                AlphaSelection(model())
-            except YellowbrickTypeError:
-                pytest.fail("could not instantiate RegressorCV on alpha selection")
+    @pytest.mark.parametrize("model", [RidgeCV, LassoCV, LassoLarsCV, ElasticNetCV])
+    def test_regressor_cv(self, model):
+        """
+        Ensure "CV" regressors are allowed
+        """
+        try:
+            AlphaSelection(model())
+        except YellowbrickTypeError:
+            pytest.fail("could not instantiate RegressorCV on alpha selection")
 
-    def test_only_regressors(self):
+    @pytest.mark.parametrize("model", [SVC, KMeans, PCA])
+    def test_only_regressors(self, model):
         """
         Assert AlphaSelection only works with regressors
         """
-        # TODO: parameterize with classifier, clusterer, decomposition
         with pytest.raises(YellowbrickTypeError):
-            AlphaSelection(SVC())
+            AlphaSelection(model())
 
     def test_store_cv_values(self):
         """
@@ -99,21 +103,19 @@ def test_store_cv_values(self):
         model = AlphaSelection(RidgeCV(store_cv_values=False))
         assert model.estimator.store_cv_values
 
-    def test_get_alphas_param(self):
+    @pytest.mark.parametrize("model", [RidgeCV, LassoCV, ElasticNetCV])
+    def test_get_alphas_param(self, model):
         """
-        Assert that we can get the alphas from ridge, lasso, and elasticnet
+        Assert that we can get the alphas from original CV models
         """
         alphas = np.logspace(-10, -2, 100)
 
-        # Test original CV models
-        # TODO: parametrize this test with different models
-        for model in (RidgeCV, LassoCV, ElasticNetCV):
-            try:
-                model = AlphaSelection(model(alphas=alphas))
-                malphas = model._find_alphas_param()
-                assert_array_equal(alphas, malphas)
-            except YellowbrickValueError:
-                pytest.fail("could not find alphas on {}".format(model.name))
+        try:
+            model = AlphaSelection(model(alphas=alphas))
+            malphas = model._find_alphas_param()
+            assert_array_equal(alphas, malphas)
+        except YellowbrickValueError:
+            pytest.fail("could not find alphas on {}".format(model.name))
 
     def test_get_alphas_param_lassolars(self):
         """
@@ -128,24 +130,21 @@ def test_get_alphas_param_lassolars(self):
         except YellowbrickValueError:
             pytest.fail("could not find alphas on {}".format(model.name))
 
-    def test_get_errors_param(self):
+    @pytest.mark.parametrize("model", [RidgeCV, LassoCV, LassoLarsCV, ElasticNetCV])
+    def test_get_errors_param(self, model):
         """
         Test known models we can get the cv errors for alpha selection
         """
+        try:
+            model = AlphaSelection(model())
 
-        # Test original CV models
-        # TODO: parametrize this test with different models
-        for model in (RidgeCV, LassoCV, LassoLarsCV, ElasticNetCV):
-            try:
-                model = AlphaSelection(model())
-
-                X, y = make_regression()
-                model.fit(X, y)
+            X, y = make_regression()
+            model.fit(X, y)
 
-                errors = model._find_errors_param()
-                assert len(errors) > 0
-            except YellowbrickValueError:
-                pytest.fail("could not find errors on {}".format(model.name))
+            errors = model._find_errors_param()
+            assert len(errors) > 0
+        except YellowbrickValueError:
+            pytest.fail("could not find errors on {}".format(model.name))
 
     def test_score(self):
         """
diff --git a/tests/test_style/test_colors.py b/tests/test_style/test_colors.py
index 5ca797145..aaf7a7def 100644
--- a/tests/test_style/test_colors.py
+++ b/tests/test_style/test_colors.py
@@ -235,7 +235,7 @@ def test_integrated_yb_colormap(self):
         )
         visualizer = SilhouetteVisualizer(KMeans(random_state=0), colormap='neural_paint')
         visualizer.fit(X)
-        visualizer.poof()
+        visualizer.finalize()
 
         tol = 3.2 if sys.platform == "win32" else 0.01  # Fails on AppVeyor with RMS 3.143
         self.assert_images_similar(visualizer, remove_legend=True, tol=tol)
diff --git a/tests/test_style/test_palettes.py b/tests/test_style/test_palettes.py
index 6ff78e71f..ec8dd2c5a 100644
--- a/tests/test_style/test_palettes.py
+++ b/tests/test_style/test_palettes.py
@@ -17,7 +17,7 @@
 ## Imports
 ##########################################################################
 
-import unittest
+import pytest
 import numpy as np
 import matplotlib as mpl
 
@@ -35,7 +35,7 @@
 ## Color Palette Tests
 ##########################################################################
 
-class ColorPaletteObjectTests(VisualTestCase):
+class TestColorPaletteObject(VisualTestCase):
     """
     Tests the ColorPalette object
     """
@@ -54,11 +54,11 @@ def test_init_palette_by_name(self):
                     "Could not instantiate {} color palette by name".format(name)
                 )
 
-            self.assertEqual(value, palette)
+            assert value == palette
 
         # Try a name not in PALETTES
-        with self.assertRaises(YellowbrickValueError):
-            self.assertNotIn('foo', PALETTES, "Cannot test bad name 'foo' it is in PALETTES!")
+        with pytest.raises(YellowbrickValueError):
+            assert 'foo' not in PALETTES, "Cannot test bad name 'foo' it is in PALETTES!"
             palette = ColorPalette('foo')
 
     def test_init_palette_by_list(self):
@@ -69,12 +69,12 @@ def test_init_palette_by_list(self):
         # Try all the values in the palettes (HEX)
         for value in PALETTES.values():
             palette = ColorPalette(value)
-            self.assertEqual(len(value), len(palette))
+            assert len(value) == len(palette)
 
         # Try all the values converted to RGB
         for value in PALETTES.values():
             palette = ColorPalette(map(mpl.colors.colorConverter.to_rgb, value))
-            self.assertEqual(len(value), len(palette))
+            assert len(value) == len(palette)
 
     def test_color_palette_context(self):
         """
@@ -84,10 +84,10 @@ def test_color_palette_context(self):
         context = color_palette('dark')
 
         with ColorPalette('dark') as palette:
-            self.assertIsInstance(palette, ColorPalette)
-            self.assertEqual(get_color_cycle(), context)
+            assert isinstance(palette, ColorPalette)
+            assert get_color_cycle() == context
 
-        self.assertEqual(get_color_cycle(), default)
+        assert get_color_cycle() == default
 
     def test_as_hex_as_rgb(self):
         """
@@ -97,16 +97,16 @@ def test_as_hex_as_rgb(self):
         expected  = PALETTES['flatui']
         morgified = palette.as_hex()
 
-        self.assertIsNot(morgified, palette)
-        self.assertIsInstance(morgified, ColorPalette)
-        self.assertEqual(morgified, expected)
+        assert morgified is not palette
+        assert isinstance(morgified, ColorPalette)
+        assert morgified == expected
 
         remorgified = morgified.as_rgb()
-        self.assertIsNot(remorgified, morgified)
-        self.assertIsNot(remorgified, palette)
-        self.assertEqual(remorgified, palette)
+        assert remorgified is not morgified
+        assert remorgified is not palette
+        assert remorgified == palette
 
-    @unittest.skip("not implemented yet")
+    @pytest.mark.skip(reason="not implemented yet")
     def test_plot_color_palette(self):
         """
         Test the plotting of a color palette for color visualization
@@ -116,7 +116,7 @@ def test_plot_color_palette(self):
         )
 
 
-class ColorPaletteFunctionTests(VisualTestCase):
+class TestColorPaletteFunction(VisualTestCase):
     """
     Tests the color_palette function.
     """
@@ -127,7 +127,7 @@ def test_current_palette(self):
         """
         pal = color_palette(["red", "blue", "green"], 3)
         set_palette(pal, 3)
-        self.assertEqual(pal, get_color_cycle())
+        assert pal == get_color_cycle()
 
         # Reset the palette
         set_aesthetic()
@@ -141,9 +141,9 @@ def test_palette_context(self):
         context_pal = color_palette("muted")
 
         with color_palette(context_pal):
-            self.assertEqual(get_color_cycle(), context_pal)
+            assert get_color_cycle() == context_pal
 
-        self.assertEqual(get_color_cycle(), default_pal)
+        assert get_color_cycle() == default_pal
 
     def test_big_palette_context(self):
         """
@@ -155,9 +155,9 @@ def test_big_palette_context(self):
 
         set_palette(original_pal)
         with color_palette(context_pal, 10):
-            self.assertEqual(get_color_cycle(), context_pal)
+            assert get_color_cycle() == context_pal
 
-        self.assertEqual(get_color_cycle(), original_pal)
+        assert get_color_cycle() == original_pal
 
         # Reset default
         set_aesthetic()
@@ -169,7 +169,7 @@ def test_yellowbrick_palettes(self):
         pals = ["accent", "dark", "pastel", "bold", "muted"]
         for name in pals:
             pal_out = color_palette(name)
-            self.assertEqual(len(pal_out), 6, "{} is not of len 6".format(name))
+            assert len(pal_out) == 6, "{} is not of len 6".format(name)
 
     def test_seaborn_palettes(self):
         """
@@ -179,7 +179,7 @@ def test_seaborn_palettes(self):
                 "sns_bright", "sns_dark", "sns_colorblind"]
         for name in pals:
             pal_out = color_palette(name)
-            self.assertEqual(len(pal_out), 6)
+            assert len(pal_out) == 6
 
     def test_other_palettes(self):
         """
@@ -188,7 +188,8 @@ def test_other_palettes(self):
         pals = ["flatui", "paired", "neural_paint", "set1"]
         for name in pals:
             pal_out = color_palette(name)
-            self.assertTrue(pal_out)
+            assert pal_out is not None
+            assert len(pal_out) > 0
 
 
     def test_bad_palette_name(self):
@@ -196,10 +197,10 @@ def test_bad_palette_name(self):
         Test that a bad palette name raises an exception
         """
 
-        with self.assertRaises(ValueError):
+        with pytest.raises(ValueError):
             color_palette("IAmNotAPalette")
 
-        with self.assertRaises(YellowbrickValueError):
+        with pytest.raises(YellowbrickValueError):
             color_palette("IAmNotAPalette")
 
     def test_bad_palette_colors(self):
@@ -208,10 +209,10 @@ def test_bad_palette_colors(self):
         """
 
         pal = ["red", "blue", "iamnotacolor"]
-        with self.assertRaises(ValueError):
+        with pytest.raises(ValueError):
             color_palette(pal)
 
-        with self.assertRaises(YellowbrickValueError):
+        with pytest.raises(YellowbrickValueError):
             color_palette(pal)
 
     def test_palette_is_list_of_tuples(self):
@@ -222,10 +223,10 @@ def test_palette_is_list_of_tuples(self):
         pal_in = np.array(["red", "blue", "green"])
         pal_out = color_palette(pal_in, 3)
 
-        self.assertIsInstance(pal_out, list)
-        self.assertIsInstance(pal_out[0], tuple)
-        self.assertIsInstance(pal_out[0][0], float)
-        self.assertEqual(len(pal_out[0]), 3)
+        assert isinstance(pal_out, list)
+        assert isinstance(pal_out[0], tuple)
+        assert isinstance(pal_out[0][0], float)
+        assert len(pal_out[0]) == 3
 
     def test_palette_cycles(self):
         """
@@ -233,20 +234,20 @@ def test_palette_cycles(self):
         """
         accent = color_palette("accent")
         double_accent = color_palette("accent", 12)
-        self.assertEqual(double_accent, accent + accent)
+        assert double_accent == accent + accent
 
-    @unittest.skip("Discovered this commented out, don't know why")
+    @pytest.mark.skip(reason="discovered this commented out, don't know why")
     def test_cbrewer_qual(self):
         """
         Test colorbrewer qualitative palettes
         """
         pal_short = mpl_palette("Set1", 4)
         pal_long = mpl_palette("Set1", 6)
-        self.assertEqual(pal_short, pal_long[:4])
+        assert pal_short == pal_long[:4]
 
         pal_full = palettes.mpl_palette("Set2", 8)
         pal_long = palettes.mpl_palette("Set2", 10)
-        self.assertEqual(pal_full, pal_long[:8])
+        assert pal_full == pal_long[:8]
 
     def test_color_codes(self):
         """
@@ -257,7 +258,7 @@ def test_color_codes(self):
         for code, color in zip("bgrmyck", colors):
             rgb_want = mpl.colors.colorConverter.to_rgb(color)
             rgb_got = mpl.colors.colorConverter.to_rgb(code)
-            self.assertEqual(rgb_want, rgb_got)
+            assert rgb_want == rgb_got
         set_color_codes("reset")
 
     def test_as_hex(self):
@@ -266,10 +267,10 @@ def test_as_hex(self):
         """
         pal = color_palette("accent")
         for rgb, hex in zip(pal, pal.as_hex()):
-            self.assertEqual(mpl.colors.rgb2hex(rgb), hex)
+            assert mpl.colors.rgb2hex(rgb) == hex
 
         for rgb_e, rgb_v in zip(pal, pal.as_hex().as_rgb()):
-            self.assertEqual(rgb_e, rgb_v)
+            assert rgb_e == rgb_v
 
     def test_preserved_palette_length(self):
         """
@@ -277,7 +278,7 @@ def test_preserved_palette_length(self):
         """
         pal_in = color_palette("Set1", 10)
         pal_out = color_palette(pal_in)
-        self.assertEqual(pal_in, pal_out)
+        assert pal_in == pal_out
 
     def test_color_sequence(self):
         """
@@ -286,33 +287,30 @@ def test_color_sequence(self):
         for name, ncols in SEQUENCES.items():
             for n in ncols.keys():
                 cmap = color_sequence(name, n)
-                self.assertEqual(name, cmap.name)
-                self.assertEqual(n, cmap.N)
+                assert name == cmap.name
+                assert n == cmap.N
 
     def test_color_sequence_default(self):
         """
         Assert the default color sequence is RdBu
         """
         cmap = color_sequence()
-        self.assertEqual(cmap.name, "RdBu")
-        self.assertEqual(cmap.N, 11)
+        assert cmap.name == "RdBu"
+        assert cmap.N == 11
 
     def test_color_sequence_unrecocognized(self):
         """
         Test value errors for unrecognized sequences
         """
-        with self.assertRaises(YellowbrickValueError):
+        with pytest.raises(YellowbrickValueError):
             color_sequence('PepperBucks', 3)
 
     def test_color_sequence_bounds(self):
         """
         Test color sequence out of bounds value error
         """
-        with self.assertRaises(YellowbrickValueError):
+        with pytest.raises(YellowbrickValueError):
             color_sequence('RdBu', 18)
 
-        with self.assertRaises(YellowbrickValueError):
+        with pytest.raises(YellowbrickValueError):
             color_sequence('RdBu', 2)
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/tests/test_style/test_rcmod.py b/tests/test_style/test_rcmod.py
index d62ca2820..9ea510424 100644
--- a/tests/test_style/test_rcmod.py
+++ b/tests/test_style/test_rcmod.py
@@ -18,13 +18,12 @@
 ## Imports
 ##########################################################################
 
-import unittest
+import pytest
 import numpy as np
 import matplotlib as mpl
 import numpy.testing as npt
 import yellowbrick.style.rcmod as yb_rcmod
 
-from distutils.version import LooseVersion
 from tests.base import VisualTestCase
 
 
@@ -38,9 +37,9 @@ class RCParamTester(VisualTestCase):
     """
 
     excluded_params = {
-        "backend",  # This cannot be changed by manipulating rc
-        "svg.embed_char_paths",  # This param causes test issues and is deprecated anyway
-        "font.family", # breaks the visualtest case
+        "backend",               # This cannot be changed by manipulating rc
+        "svg.embed_char_paths",  # This param causes test issues and is deprecated
+        "font.family",           # breaks the visualtest case
     }
 
     def flatten_list(self, orig_list):
@@ -57,7 +56,7 @@ def assert_rc_params(self, params):
             elif isinstance(v, np.ndarray):
                 npt.assert_array_equal(mpl.rcParams[k], v)
             else:
-                self.assertEqual((k, mpl.rcParams[k]), (k, v))
+                assert (k, mpl.rcParams[k]) == (k, v)
 
 
 ##########################################################################
@@ -80,8 +79,8 @@ def test_rc_override(self):
 
         rc = {"axes.facecolor": "blue", "foo.notaparam": "bar"}
         out = yb_rcmod._axes_style("darkgrid", rc)
-        self.assertEqual(out["axes.facecolor"], "blue")
-        self.assertNotIn("foo.notaparam", out)
+        assert out["axes.facecolor"] == "blue"
+        assert "foo.notaparam" not in out
 
     def test_set_style(self):
         """
@@ -91,7 +90,7 @@ def test_set_style(self):
         yb_rcmod.set_style()
         self.assert_rc_params(style_dict)
 
-    @unittest.skip("This test doesn't make sense without multiple styles")
+    @pytest.mark.skip(reason="this test doesn't make sense without multiple styles")
     def test_style_context_manager(self):
 
         yb_rcmod.set_style("darkgrid")
@@ -112,25 +111,20 @@ def test_style_context_independence(self):
         """
         Assert context and style independence
         """
-        self.assertTrue(set(yb_rcmod._style_keys) ^ set(yb_rcmod._context_keys))
+        assert len(set(yb_rcmod._style_keys) ^ set(yb_rcmod._context_keys)) > 0
 
     def test_set_rc(self):
         """
         Test the ability to set the mpl configuration rc dict
         """
         yb_rcmod.set_aesthetic(rc={"lines.linewidth": 4})
-        self.assertEqual(mpl.rcParams["lines.linewidth"], 4)
+        assert mpl.rcParams["lines.linewidth"] == 4
         yb_rcmod.set_aesthetic()
 
     def test_reset_defaults(self):
         """
         Test the ability to reset to the mpl defaults
         """
-        # Changes to the rc parameters make this test hard to manage
-        # on older versions of matplotlib, so we'll skip it
-        if LooseVersion(mpl.__version__) < LooseVersion("1.3"):
-            raise self.SkipTest
-
         yb_rcmod.reset_defaults()
         self.assert_rc_params(mpl.rcParamsDefault)
         yb_rcmod.set_aesthetic()
@@ -139,12 +133,6 @@ def test_reset_orig(self):
         """
         Test the ability to reset to the original (respecting custom styles)
         """
-
-        # Changes to the rc parameters make this test hard to manage
-        # on older versions of matplotlib, so we'll skip it
-        if LooseVersion(mpl.__version__) < LooseVersion("1.3"):
-            raise self.SkipTest
-
         yb_rcmod.reset_orig()
         self.assert_rc_params(mpl.rcParamsOrig)
         yb_rcmod.set_aesthetic()
@@ -171,7 +159,7 @@ def test_font_scale(self):
                      "xtick.labelsize", "ytick.labelsize", "font.size"]
 
         for k in font_keys:
-            self.assertEqual(notebook_ref[k] * 2, notebook_big[k])
+            assert notebook_ref[k] * 2 == notebook_big[k]
 
     def test_rc_override(self):
         """
@@ -180,8 +168,8 @@ def test_rc_override(self):
         key, val = "grid.linewidth", 5
         rc = {key: val, "foo": "bar"}
         out = yb_rcmod._plotting_context("talk", rc=rc)
-        self.assertEqual(out[key], val)
-        self.assertNotIn("foo", out)
+        assert out[key] == val
+        assert "foo" not in out
 
     def test__set_context(self):
         """
@@ -191,7 +179,7 @@ def test__set_context(self):
         yb_rcmod._set_context()
         self.assert_rc_params(context_dict)
 
-    @unittest.skip("This test doesn't make sense without multiple contexts")
+    @pytest.mark.skip(reason="this test doesn't make sense without multiple contexts")
     def test_context_context_manager(self):
 
         yb_rcmod._set_context("notebook")
@@ -208,6 +196,3 @@ def func():
         func()
         self.assert_rc_params(orig_params)
 
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/tests/test_target/test_binning.py b/tests/test_target/test_binning.py
index 1f55c4cc9..df7317b6b 100644
--- a/tests/test_target/test_binning.py
+++ b/tests/test_target/test_binning.py
@@ -1,7 +1,7 @@
 # tests.test_target.test_binning
 # Tests for the BalancedBinningReference visualizer
 #
-# Author:   Juan L. Kehoe (juanluo2008@gmail.com)
+# Author:  Juan L. Kehoe (juanluo2008@gmail.com)
 # Author:  Prema Damodaran Roman (pdamo24@gmail.com)
 # Created: Thu Jul 20 10:21:49 2018 -0400
 #
@@ -11,29 +11,28 @@
 from tests.dataset import DatasetMixin
 from yellowbrick.target.binning import *
 
+
 ##########################################################################
 ## BalancedBinningReference Tests
 ##########################################################################
 
 class TestBalancedBinningReference(VisualTestCase, DatasetMixin):
-	"""
-	Test the BalancedBinningReference visualizer 
-	"""
-
-	def test_balancedbinningreference(self):
-		"""
-		Test Histogram on a real dataset
-		"""
-		# Load the data from the fixture
-		dataset = self.load_data('occupancy')
-
-		# Get the data
-		y = dataset["temperature"]
-
-		
-		visualizer = BalancedBinningReference()
-		visualizer.fit(y)
-		visualizer.finalize()
-		self.assert_images_similar(visualizer, tol=0.5)
-			
-		
\ No newline at end of file
+    """
+    Test the BalancedBinningReference visualizer
+    """
+
+    def test_balancedbinningreference(self):
+        """
+        Test Histogram on a real dataset
+        """
+        # Load the data from the fixture
+        dataset = self.load_data('occupancy')
+
+        # Get the data
+        y = dataset["temperature"]
+
+        visualizer = BalancedBinningReference()
+        visualizer.fit(y)
+        visualizer.finalize()
+        self.assert_images_similar(visualizer, tol=0.5)
+
diff --git a/tests/test_target/test_class_balance.py b/tests/test_target/test_class_balance.py
index ded124a4a..015c06e5d 100644
--- a/tests/test_target/test_class_balance.py
+++ b/tests/test_target/test_class_balance.py
@@ -66,7 +66,7 @@ def make_fixture(binary=False, balanced=False, split=False):
 ##  Tests
 ##########################################################################
 
-class ClassBalanceTests(VisualTestCase, DatasetMixin):
+class TestClassBalance(VisualTestCase, DatasetMixin):
     """
     Test ClassBalance visualizer
     """
diff --git a/tests/test_target/test_feature_correlation.py b/tests/test_target/test_feature_correlation.py
index de357550d..dcd06edd1 100644
--- a/tests/test_target/test_feature_correlation.py
+++ b/tests/test_target/test_feature_correlation.py
@@ -20,10 +20,6 @@
 import sys
 import pytest
 import numpy as np
-try:
-    import pandas as pd
-except ImportError:
-    pd = None
 import numpy.testing as npt
 import matplotlib.pyplot as plt
 
@@ -31,9 +27,13 @@
 from yellowbrick.exceptions import YellowbrickValueError, YellowbrickWarning
 
 from sklearn import datasets
-
 from tests.base import VisualTestCase
 
+try:
+    import pandas as pd
+except ImportError:
+    pd = None
+
 
 ##########################################################################
 ## Feature Correlation Tests
diff --git a/tests/test_text/test_base.py b/tests/test_text/test_base.py
index f913b537e..855d32d2c 100644
--- a/tests/test_text/test_base.py
+++ b/tests/test_text/test_base.py
@@ -17,8 +17,6 @@
 ## Imports
 ##########################################################################
 
-import unittest
-
 from yellowbrick.base import *
 from yellowbrick.text.base import *
 from sklearn.base import BaseEstimator, TransformerMixin
@@ -28,22 +26,13 @@
 ## TextVisualizer Base Tests
 ##########################################################################
 
-class TextVisualizerBaseTests(unittest.TestCase):
+class TestTextVisualizerBase(object):
 
     def test_subclass(self):
         """
-        Assert the text visualizer is subclassed correctly 
+        Assert the text visualizer is subclassed correctly
         """
         visualizer = TextVisualizer()
-        self.assertIsInstance(visualizer, TransformerMixin)
-        self.assertIsInstance(visualizer, BaseEstimator)
-        self.assertIsInstance(visualizer, Visualizer)
-
-    # def test_interface(self):
-    #     """
-    #     Test the feature visualizer interface
-    #     """
-    #
-    #     visualizer = TextVisualizer()
-    #     with self.assertRaises(NotImplementedError):
-    #         visualizer.poof()
+        assert isinstance(visualizer, TransformerMixin)
+        assert isinstance(visualizer, BaseEstimator)
+        assert isinstance(visualizer, Visualizer)
diff --git a/tests/test_text/test_dispersion.py b/tests/test_text/test_dispersion.py
index c6bda43e1..19dee945d 100644
--- a/tests/test_text/test_dispersion.py
+++ b/tests/test_text/test_dispersion.py
@@ -19,12 +19,13 @@
 ##########################################################################
 
 import pytest
+import matplotlib.pyplot as plt
 
 from yellowbrick.exceptions import YellowbrickValueError
 from yellowbrick.datasets import load_hobbies
 from yellowbrick.text.dispersion import *
 from tests.base import VisualTestCase
-import matplotlib.pyplot as plt
+
 
 ##########################################################################
 ## Data
@@ -36,7 +37,7 @@
 ## DispersionPlot Tests
 ##########################################################################
 
-class DispersionPlotTests(VisualTestCase):
+class TestDispersionPlot(VisualTestCase):
 
     def test_quick_method(self):
         """
@@ -107,7 +108,6 @@ def test_dispersion_plot_annotate_docs(self):
 
         self.assert_images_similar(visualizer, tol=25.5)
 
-
     def test_dispersion_plot_color_by_class(self):
         """
         Assert no errors occur during DispersionPlot integration
diff --git a/tests/test_text/test_freqdist.py b/tests/test_text/test_freqdist.py
index 5e654ea3c..29ebe25ec 100644
--- a/tests/test_text/test_freqdist.py
+++ b/tests/test_text/test_freqdist.py
@@ -35,7 +35,7 @@
 ## FreqDist Tests
 ##########################################################################
 
-class FreqDistTests(VisualTestCase):
+class TestFreqDist(VisualTestCase):
 
     @pytest.mark.xfail(
         IS_WINDOWS_OR_CONDA,
diff --git a/tests/test_text/test_postag.py b/tests/test_text/test_postag.py
index f2f66446b..a8a4bd0a9 100644
--- a/tests/test_text/test_postag.py
+++ b/tests/test_text/test_postag.py
@@ -37,6 +37,7 @@
 except ImportError:
     spacy = None
 
+
 ##########################################################################
 ## Data
 ##########################################################################
@@ -201,11 +202,11 @@ def test_frequency_mode(self):
         viz = postag(tagged_docs, ax=ax, frequency=True)
         viz.finalize()
         ax.grid(False)
-        
+
         # Sorted tags i.e predetermined order
         sorted_tags = ['noun', 'adjective', 'punctuation', 'verb', 'preposition',
-                       'determiner', 'adverb', 'conjunction', 'pronoun', 'wh- word', 
-                       'modal', 'infinitive', 'possessive', 'other', 'symbol', 
+                       'determiner', 'adverb', 'conjunction', 'pronoun', 'wh- word',
+                       'modal', 'infinitive', 'possessive', 'other', 'symbol',
                        'existential', 'digit', 'non-English', 'interjection', 'list']
         # Extract tick labels from the plot
         ticks_ax = [tick.get_text() for tick in ax.xaxis.get_ticklabels()]
@@ -284,10 +285,10 @@ def test_stack_mode(self):
         visualizer.ax.grid(False)
 
         self.assert_images_similar(ax=ax)
-        
+
     def test_stack_frequency_mode(self):
         """
-        Assert no errors occur when the visualizer is run on both stack and 
+        Assert no errors occur when the visualizer is run on both stack and
         frequency mode
         """
         check_nltk_data()
@@ -298,11 +299,11 @@ def test_stack_frequency_mode(self):
         visualizer = PosTagVisualizer(stack=True, frequency=True, ax=ax)
         visualizer.fit(tagged_docs, y=['a','b','c'])
         visualizer.ax.grid(False)
-        
+
         # Sorted tags i.e predetermined order
         sorted_tags = ['noun', 'adjective', 'punctuation', 'verb', 'preposition',
-                       'determiner', 'adverb', 'conjunction', 'pronoun', 'wh- word', 
-                       'modal', 'infinitive', 'possessive', 'other', 'symbol', 
+                       'determiner', 'adverb', 'conjunction', 'pronoun', 'wh- word',
+                       'modal', 'infinitive', 'possessive', 'other', 'symbol',
                        'existential', 'digit', 'non-English', 'interjection', 'list']
         # Extract tick labels from the plot
         ticks_ax = [tick.get_text() for tick in ax.xaxis.get_ticklabels()]
diff --git a/tests/test_text/test_tsne.py b/tests/test_text/test_tsne.py
index 8fee67cd2..dc56d554d 100644
--- a/tests/test_text/test_tsne.py
+++ b/tests/test_text/test_tsne.py
@@ -41,6 +41,7 @@
 
 corpus = load_hobbies()
 
+
 ##########################################################################
 ## TSNE Tests
 ##########################################################################
@@ -132,16 +133,16 @@ def test_custom_colors_tsne(self):
         """
         ## produce random data
         X, y = make_classification(n_samples=200, n_features=100,
-                               n_informative=20, n_redundant=10, 
+                               n_informative=20, n_redundant=10,
                                n_classes=5, random_state=42)
-        
+
         ## specify a list of custom colors >= n_classes
         purple_blues = ["indigo", "orchid", "plum", "navy", "purple", "blue"]
-        
+
         ## instantiate the visualizer and check that self.colors is correct
         purple_tsne = TSNEVisualizer(colors=purple_blues, random_state=87)
         assert purple_tsne.colors == purple_blues
-        
+
         ## fit the visualizer and check that self.color_values is as long as
         ## n_classes and is the first n_classes items in self.colors
         purple_tsne.fit(X,y)
diff --git a/tests/test_text/test_umap.py b/tests/test_text/test_umap.py
index c9798831b..4bce784fb 100644
--- a/tests/test_text/test_umap.py
+++ b/tests/test_text/test_umap.py
@@ -51,6 +51,7 @@
 
 corpus = load_hobbies()
 
+
 ##########################################################################
 ## UMAP Tests
 ##########################################################################
@@ -143,16 +144,16 @@ def test_custom_colors_umap(self):
         """
         ## produce random data
         X, y = make_classification(n_samples=200, n_features=100,
-                               n_informative=20, n_redundant=10, 
+                               n_informative=20, n_redundant=10,
                                n_classes=5, random_state=42)
-        
+
         ## specify a list of custom colors >= n_classes
         purple_blues = ["indigo", "orchid", "plum", "navy", "purple", "blue"]
-        
+
         ## instantiate the visualizer and check that self.colors is correct
         purple_umap = UMAPVisualizer(colors=purple_blues, random_state=87)
         assert purple_umap.colors == purple_blues
-        
+
         ## fit the visualizer and check that self.color_values is as long as
         ## n_classes and is the first n_classes items in self.colors
         purple_umap.fit(X,y)
diff --git a/tests/test_utils/test_decorators.py b/tests/test_utils/test_decorators.py
index 99e6a477f..c5c9c98f5 100644
--- a/tests/test_utils/test_decorators.py
+++ b/tests/test_utils/test_decorators.py
@@ -17,8 +17,6 @@
 ## Imports
 ##########################################################################
 
-import unittest
-
 from yellowbrick.utils.decorators import *
 
 
@@ -26,7 +24,7 @@
 ## Decorator Tests
 ##########################################################################
 
-class DecoratorTests(unittest.TestCase):
+class TestDecorators(object):
     """
     Tests for the decorator utilities.
     """
@@ -43,10 +41,9 @@ def foo(self):
                 return "bar"
 
         viz = Visualizer()
-        self.assertFalse(hasattr(viz, "_foo"))
-        self.assertEqual(viz.foo, "bar")
-        self.assertEqual(viz._foo, "bar")
-
+        assert not hasattr(viz, "_foo")
+        assert viz.foo == "bar"
+        assert viz._foo == "bar"
 
     def test_docutil(self):
         """
@@ -69,34 +66,18 @@ def undecorated(*args, **kwargs):
             pass
 
         # Test the undecorated string to protect from magic
-        self.assertEqual(
-            undecorated.__doc__.strip(), "This is an undecorated function string."
-        )
+        assert undecorated.__doc__.strip() == "This is an undecorated function string."
 
         # Decorate manually and test the newly decorated return function.
         decorated = docutil(Visualizer.__init__)(undecorated)
-        self.assertEqual(
-            decorated.__doc__.strip(), "This is the correct docstring."
-        )
+        assert decorated.__doc__.strip() == "This is the correct docstring."
 
         # Assert that decoration modifies the original function.
-        self.assertEqual(
-            undecorated.__doc__.strip(), "This is the correct docstring."
-        )
+        assert undecorated.__doc__.strip() == "This is the correct docstring."
 
         @docutil(Visualizer.__init__)
         def sugar(*args, **kwargs):
             pass
 
         # Assert that syntactic sugar works as expected.
-        self.assertEqual(
-            sugar.__doc__.strip(), "This is the correct docstring."
-        )
-
-
-##########################################################################
-## Execute Tests
-##########################################################################
-
-if __name__ == "__main__":
-    unittest.main()
+        assert sugar.__doc__.strip() == "This is the correct docstring."
diff --git a/tests/test_utils/test_helpers.py b/tests/test_utils/test_helpers.py
index 6c4fe9f62..b276a97f3 100644
--- a/tests/test_utils/test_helpers.py
+++ b/tests/test_utils/test_helpers.py
@@ -81,7 +81,6 @@ def test_str_input(self):
 ## Numeric Function Tests
 ##########################################################################
 
-
 class TestNumericFunctions(object):
     """
     Numeric helper functions
diff --git a/tests/test_utils/test_kneed.py b/tests/test_utils/test_kneed.py
index e0e788a9c..c9134c711 100644
--- a/tests/test_utils/test_kneed.py
+++ b/tests/test_utils/test_kneed.py
@@ -2,22 +2,22 @@
 # A port of the tests for knee-point detection package, kneed.
 #
 # Author:   Kevin Arvai
-# Author:   Pradeep Singh 
+# Author:   Pradeep Singh
 # Created:  Mon Apr 23 01:29:18 2019 -0400
 #
 # Copyright (C) 2017 Kevin Arvai
 # All rights reserved.
-# Redistribution and use in source and binary forms, with or without modification, 
+# Redistribution and use in source and binary forms, with or without modification,
 # are permitted provided that the following conditions are met:
-# 
+#
 # 1. Redistributions of source code must retain the above copyright notice, this list
 # of conditions and the following disclaimer.
 #
-# 2. Redistributions in binary form must reproduce the above copyright notice, this 
-# list of conditions and the following disclaimer in the documentation and/or other 
+# 2. Redistributions in binary form must reproduce the above copyright notice, this
+# list of conditions and the following disclaimer in the documentation and/or other
 # materials provided with the distribution.
 #
-# 3. Neither the name of the copyright holder nor the names of its contributors may 
+# 3. Neither the name of the copyright holder nor the names of its contributors may
 # be used to endorse or promote products derived from this software without specific
 # prior written permission.
 #
@@ -26,7 +26,7 @@
 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS 
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
@@ -35,10 +35,11 @@
 # ID: test_kneed.py [] pswaldia@no-reply.github.com $
 
 """
-This package contains a port of the tests for knee-point detection package, kneed, by 
-Kevin Arvai and hosted at https://github.com/arvkevi/kneed. This port is maintained 
+This package contains a port of the tests for knee-point detection package, kneed, by
+Kevin Arvai and hosted at https://github.com/arvkevi/kneed. This port is maintained
 with permission by the Yellowbrick contributors.
 """
+
 import numpy as np
 from yellowbrick.utils.kneed import KneeLocator
 
@@ -50,28 +51,28 @@
 
 
 def test_concave_increasing():
-    """Tests that a correct knee point is detected in 
+    """Tests that a correct knee point is detected in
     curve having concave and increasing nature."""
     kn = KneeLocator(x, y_concave_inc, curve_nature='concave', curve_direction='increasing')
     assert kn.knee == 2
 
 
 def test_concave_decreasing():
-    """Tests that a correct knee point is detected in 
+    """Tests that a correct knee point is detected in
     curve having concave and decreasing nature."""
     kn = KneeLocator(x, y_concave_dec, curve_nature='concave', curve_direction='decreasing')
     assert kn.knee == 7
 
 
 def test_convex_increasing():
-    """Tests that a correct knee point is detected in 
+    """Tests that a correct knee point is detected in
     curve having convex and increasing nature."""
     kn = KneeLocator(x, y_convex_inc, curve_nature='convex', curve_direction='increasing')
     assert kn.knee == 7
 
 
 def test_convex_decreasing():
-    """Tests that a correct knee point is detected in 
+    """Tests that a correct knee point is detected in
     curve having convex and decreasing nature."""
     kn = KneeLocator(x, y_convex_dec, curve_nature='convex', curve_direction='decreasing')
     assert kn.knee == 2
diff --git a/tests/test_utils/test_timer.py b/tests/test_utils/test_timer.py
index 5f7cbf719..8f1334511 100644
--- a/tests/test_utils/test_timer.py
+++ b/tests/test_utils/test_timer.py
@@ -19,6 +19,7 @@
 from unittest import mock
 from yellowbrick.utils.timer import *
 
+
 ##########################################################################
 ## Helper Function Tests
 ##########################################################################
diff --git a/yellowbrick/features/rfecv.py b/yellowbrick/features/rfecv.py
index e224bd8f1..e68af5c75 100644
--- a/yellowbrick/features/rfecv.py
+++ b/yellowbrick/features/rfecv.py
@@ -171,7 +171,7 @@ def fit(self, X, y=None):
         else:
             step = int(self.step)
 
-        if step < 0:
+        if step <= 0:
             raise YellowbrickValueError("step must be >0")
 
         # Create the RFE model

Overhaul unittest and fixtures
Remove dependency on `unittest.TestCase` by replacing assertions with `assert` statements. This also involves using `pytest.raises` and `pytest.skip` decorators. 

Use PyTest fixtures for datasets and other fixtures. 

~Add PEP checking to pytest (alongside pyflakes)~

- [x] rename `ThingTests` to `TestThing` 
- [x] assertions 
- [x] pytest.skip 
- [x] dataset fixtures 
- [ ] ~PEP8~

Alongside this we should also do a PY2 removal check, namely anywhere:

```python
try:
    from unittest import mock
except ImportError:
    import mock
```

should be replaced with just `from unittest import mock` 
