Repository: census-instrumentation/opencensus-python
Pull request #149 (2018-04-06 22:42:03)

[WIP] Implementation of Python Stats API

googlebot: 
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

:memo: **Please visit <https://cla.developers.google.com/> to sign.**

Once you've signed (or fixed any issues), please reply here (e.g. `I signed it!`) and we'll verify it.

----

#### What to do if you already signed the CLA

##### Individual signers

*   It's possible we don't have your GitHub username or you're using a different email address on your commit. Check [your existing CLA data](https://cla.developers.google.com/clas) and verify that your [email is set on your git commits](https://help.github.com/articles/setting-your-email-in-git/).

##### Corporate signers

*   Your company has a Point of Contact who decides which employees are authorized to participate. Ask your POC to be added to the group of authorized contributors. If you don't know who your Point of Contact is, direct the Google project maintainer to [go/cla#troubleshoot](go/cla#troubleshoot) ([Public version](https://opensource.google.com/docs/cla/#troubleshoot)).
*   The email used to register you as an authorized contributor must be the email used for the Git commit. Check [your existing CLA data](https://cla.developers.google.com/clas) and verify that your [email is set on your git commits](https://help.github.com/articles/setting-your-email-in-git/).
*   The email used to register you as an authorized contributor must also be [attached to your GitHub account](https://github.com/settings/emails).
		

<!-- need_sender_cla -->
kcooperstein: I signed it!
kcooperstein: I signed it!
kcooperstein: I signed it!
googlebot: CLAs look good, thanks!

<!-- ok -->
liyanhui1228: FYI I updated the `nox.py` config to enable it run on both trace and stats directory, please fix the lint warnings and test coverage.
liyanhui1228: Adding @songy23 for sanity check.
liyanhui1228: I signed it!
liyanhui1228: We can ignore the CLA, merging...

Patch:
diff --git a/.gitignore b/.gitignore
index 3f51251..a8f5c23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ develop-eggs
 lib
 lib64
 __pycache__
+venv/
 
 # Installer logs
 pip-log.txt
@@ -53,6 +54,7 @@ docs/_build_doc2dash
 
 # Virtual environment
 env/
+venv/
 coverage.xml
 
 # System test environment variables.
diff --git a/docs/trace/usage.rst b/docs/trace/usage.rst
index f05b7d7..9fdf818 100644
--- a/docs/trace/usage.rst
+++ b/docs/trace/usage.rst
@@ -1,5 +1,5 @@
-OpenCensus Trace for Python
-===========================
+OpenCensus for Python - A stats collection and distributed tracing framework
+============================================================================
 
     `Census`_ for Python. Census provides a framework to measure a server's resource
     usage and collect performance stats. This repository contains Python related
@@ -14,7 +14,7 @@ OpenCensus Trace for Python
 
 -  `API Documentation`_
 
-.. _API Documentation: http://opencensus.io/opencensus-python/trace/usage.html
+.. _API Documentation: https://opencensus.io/api/python/trace/usage.html
 
 Installation & basic usage
 --------------------------
@@ -23,14 +23,10 @@ Installation & basic usage
 
     ::
 
-        pip install opencensus-trace
-        pipenv install opencensus-trace
+        pip install opencensus
+        pipenv install opencensus
 
-        # install the Stackdriver exporter by including the extras package
-        pip install opencensus-trace[stackdriver]
-        pipenv install opencensus-trace[stackdriver]
-
-2. Initialize a tracer for application:
+2. Initialize a tracer for your application:
 
     .. code:: python
 
@@ -135,7 +131,7 @@ Propagators
 ~~~~~~~~~~~
 
 You can specify the propagator type for serializing and deserializing the
-``SpanContex`` and its headers. There are currently two built in propagators:
+``SpanContext`` and its headers. There are currently two built in propagators:
 ``GoogleCloudFormatPropagator`` and ``TextFormatPropagator``.
 
 This example shows how to use the ``GoogleCloudFormatPropagator``:
@@ -186,8 +182,8 @@ For Django, you can configure the blacklist in the ``OPENCENSUS_PARAMS`` in ``se
 Framework Integration
 ---------------------
 
-Opencensus supports integration with popular web frameworks including
-Django, Flask, and Webapp2. When the application receives a HTTP request,
+Census supports integration with popular web frameworks including
+Django, Flask, Pyramid, and Webapp2. When the application receives a HTTP request,
 the tracer will automatically generate a span context using the trace
 information extracted from the request headers, and propagated to the
 child spans.
@@ -209,41 +205,6 @@ requests will be automatically traced.
     # `GoogleCloudFormatPropagator` as propagator.
     middleware = FlaskMiddleware(app)
 
-The middleware also supports the factory pattern.
-
-.. code:: python
-
-    from opencensus.trace.ext.flask.flask_middleware import FlaskMiddleware
-    middleware = FlaskMiddleware()
-    app = flask.Flask(__name__)
-    middleware.init_app(app)
-
-The Flask application configuration can also be used to specify the sampler,
-exporter, propagator in the middleware.
-
-.. code:: python
-
-    OPENCENSUS_TRACE = {
-        'SAMPLER': opencensus.trace.samplers.probability.ProbabilitySampler,
-        'REPORTER': opencensus.trace.exporters.print_exporter.PrintExporter,
-        'PROPAGATOR': opencensus.trace.propagation.google_cloud_format.
-                      GoogleCloudFormatPropagator,
-    }
-
-You can configure the sampling rate and other parameters using the ``OPENCENSUS_TRACE_PARAMS``
-key in the app configuration:
-
-.. code:: python
-
-    OPENCENSUS_TRACE_PARAMS = {
-        'BLACKLIST_PATHS': ['/_ah/health'],
-        'GCP_EXPORTER_PROJECT': None,
-        'SAMPLING_RATE': 0.5,
-        'ZIPKIN_EXPORTER_SERVICE_NAME': 'my_service',
-        'ZIPKIN_EXPORTER_HOST_NAME': 'localhost',
-        'ZIPKIN_EXPORTER_PORT': 9411,
-    }
-
 Django
 ~~~~~~
 
@@ -266,7 +227,7 @@ And add this line to the ``INSTALLED_APPS`` section:
         'opencensus.trace.ext.django',
     ]
 
-You can configure the the sampler, exporter, propagator using the ``OPENCENSUS_TRACE`` setting in
+You can configure the sampler, exporter, propagator using the ``OPENCENSUS_TRACE`` setting in
 ``settings.py``:
 
 .. code:: python
@@ -292,18 +253,56 @@ setting in ``settings.py``:
         'ZIPKIN_EXPORTER_PORT': 9411,
     }
 
-Webapp2
+
+Pyramid
 ~~~~~~~
 
+In your application, add the pyramid tween and your requests will be
+traced.
+
 .. code:: python
 
-    from opencensus.trace.tracers import webapp2_tracer
+    def main(global_config, **settings):
+        config = Configurator(settings=settings)
 
-    tracer = webapp2_tracer.WebApp2Tracer()
+        config.add_tween('opencensus.trace.ext.pyramid'
+                         '.pyramid_middleware.OpenCensusTweenFactory')
 
-    with tracer.span(name='span1'):
-        do_something_to_trace()
+To configure the sampler, exporter, and propagator, pass the instances
+into the pyramid settings
+
+.. code:: python
 
+    from opencensus.trace.exporters import print_exporter
+    from opencensus.trace.propagation import google_cloud_format
+    from opencensus.trace.samplers import probability
+
+    settings = {}
+    settings['OPENCENSUS_TRACE'] = {
+        'EXPORTER': print_exporter.PrintExporter(),
+        'SAMPLER': probability.ProbabilitySampler(rate=0.5),
+        'PROPAGATOR': google_cloud_format.GoogleCloudFormatPropagator(),
+    }
+
+    config = Configurator(settings=settings)
+
+gRPC Integration
+----------------
+
+OpenCensus provides the implementation of interceptors for both the client side
+and server side to instrument the gRPC requests and responses. The client
+interceptors are used to create a decorated channel that intercepts client
+gRPC calls and server interceptors act as decorators over handlers.
+
+gRPC interceptor is a new feature in the grpcio1.8.0 release, please upgrade
+your grpcio to the latest version to use this feature.
+
+For sample usage, please refer to the hello world example in the examples
+directory.
+
+More information about the gRPC interceptors please see the `proposal`_.
+
+.. _proposal: https://github.com/mehrdada/proposal/blob/python-interceptors/L13-Python-Interceptors.md
 
 Service Integration
 -------------------
@@ -345,22 +344,24 @@ to ``trace_integrations`` using ``'postgresql'``.
 SQLAlchemy
 ~~~~~~~~~~
 
-You can trace usage of `sqlalchemy package`_, regardless of the underlying database, by
-specifying ``'sqlalchemy'`` to ``trace_integrations``.
+You can trace usage of the `sqlalchemy package`_, regardless of the underlying
+database, by specifying ``'sqlalchemy'`` to ``trace_integrations``.
 
 .. _SQLAlchemy package: https://pypi.org/project/SQLAlchemy
 
-.. note:: If you enable tracing of SQLAlchemy and the underlying database
+.. note:: If you enable tracing of SQLAlchemy as well as the underlying database
     driver, you will get duplicate spans. Instead, just trace SQLAlchemy.
 
 Requests
 ~~~~~~~~
 
-Census can trace HTTP requests made with the `Requests`_ library. The request URL,
+Census can trace HTTP requests made with the `Requests package`_. The request URL,
 method, and status will be collected.
 
 You can enable Requests integration by specifying ``'requests'`` to ``trace_integrations``.
 
+.. _Requests package: https://pypi.python.org/pypi/requests
+
 
 Contributing
 ------------
diff --git a/opencensus/stats/__init__.py b/opencensus/stats/__init__.py
new file mode 100644
index 00000000..ebe2afa
--- /dev/null
+++ b/opencensus/stats/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/opencensus/stats/aggregation.py b/opencensus/stats/aggregation.py
new file mode 100644
index 00000000..ff7f09e
--- /dev/null
+++ b/opencensus/stats/aggregation.py
@@ -0,0 +1,126 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from opencensus.stats import bucket_boundaries
+from opencensus.stats import aggregation_data
+
+
+class BaseAggregation(object):
+    """Aggregation describes how the data collected is aggregated by type of
+    aggregation and buckets
+
+    :type buckets: list(:class: '~opencensus.stats.bucket_boundaries.
+                                BucketBoundaries')
+    :param buckets: list of endpoints if the aggregation represents a
+                    distribution
+
+    """
+    def __init__(self, buckets=None):
+        self._aggregation_type = "none"
+        self._buckets = buckets or []
+
+    @property
+    def aggregation_type(self):
+        """The aggregation type of the current aggregation"""
+        return self._aggregation_type
+
+    @property
+    def buckets(self):
+        """The buckets of the current aggregation"""
+        return self._buckets
+
+
+class SumAggregation(BaseAggregation):
+    """Sum Aggregation escribes that data collected and aggregated with this
+    method will be summed
+
+    :type sum: int or float
+    :param sum: the sum of the data collected and aggregated
+
+    """
+    def __init__(self, sum=None):
+        super(SumAggregation, self).__init__()
+        self._aggregation_type = "sum"
+        if sum is not None:
+            self._sum = aggregation_data.SumAggregationDataFloat(sum_data=sum)
+        else:
+            self._sum = aggregation_data.SumAggregationDataFloat(sum_data=0)
+
+    @property
+    def aggregation_type(self):
+        """The aggregation type of the current aggregation"""
+        return self._aggregation_type
+
+    @property
+    def sum(self):
+        """The sum of the current aggregation"""
+        return self._sum
+
+
+class CountAggregation(BaseAggregation):
+    """Describes that the data collected and aggregated with this method will
+    be turned into a count value
+
+    :type count: int
+    :param count: represents the count of this aggregation
+
+    """
+    def __init__(self, count=0):
+        super(CountAggregation, self).__init__()
+        self._aggregation_type = "count"
+        self._count = aggregation_data.CountAggregationData(count)
+
+    @property
+    def aggregation_type(self):
+        """The aggregation type of the current aggregation"""
+        return self._aggregation_type
+
+    @property
+    def count(self):
+        """The count of the current aggregation"""
+        return self._count
+
+
+class DistributionAggregation(BaseAggregation):
+    """Distribution Aggregation indicates that the desired aggregation is a
+    histogram distribution
+
+    :type boundaries: list(:class:'~opencensus.stats.bucket_boundaries.
+                            BucketBoundaries')
+    :param boundaries: the bucket endpoints
+
+    :type distribution: histogram
+    :param distribution: histogram of the values of the population
+
+    """
+    def __init__(self, boundaries=None, distribution=None):
+        super(DistributionAggregation, self).__init__(boundaries)
+        self._aggregation_type = "distribution"
+        self._boundaries = bucket_boundaries.BucketBoundaries(boundaries)
+        self._distribution = distribution if distribution is not None else {}
+
+    @property
+    def aggregation_type(self):
+        """The aggregation type of the current aggregation"""
+        return self._aggregation_type
+
+    @property
+    def boundaries(self):
+        """The boundaries of the current aggregation"""
+        return self._boundaries
+
+    @property
+    def distribution(self):
+        """The distribution of the current aggregation"""
+        return self._distribution
diff --git a/opencensus/stats/aggregation_data.py b/opencensus/stats/aggregation_data.py
new file mode 100644
index 00000000..21e1dfd
--- /dev/null
+++ b/opencensus/stats/aggregation_data.py
@@ -0,0 +1,202 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from opencensus.stats import bucket_boundaries
+
+
+class BaseAggregationData(object):
+    """Aggregation Data represents an aggregated value from a collection
+
+    :type aggregation_data: aggregated value
+    :param aggregation_data: represents the aggregated value from a collection
+
+    """
+    def __init__(self, aggregation_data):
+        self._aggregation_data = aggregation_data
+
+    @property
+    def aggregation_data(self):
+        """The current aggregation data"""
+        return self._aggregation_data
+
+
+class SumAggregationDataFloat(BaseAggregationData):
+    """Sum Aggregation Data is the aggregated data for the Sum aggregation
+
+    :type sum_data: float
+    :param sum_data: represents the aggregated sum
+
+    """
+    def __init__(self, sum_data):
+        super(SumAggregationDataFloat, self).__init__(sum_data)
+        self._sum_data = sum_data
+
+    def add_sample(self, value):
+        """Allows the user to add a sample to the Sum Aggregation Data
+        The value of the sample is then added to the current sum data
+        """
+        self._sum_data += value
+
+    @property
+    def sum_data(self):
+        """The current sum data"""
+        return self._sum_data
+
+
+class CountAggregationData(BaseAggregationData):
+    """Count Aggregation Data is the count value of aggregated data
+
+    :type count_data: long
+    :param count_data: represents the aggregated count
+
+    """
+    def __init__(self, count_data):
+        super(CountAggregationData, self).__init__(count_data)
+        self._count_data = count_data
+
+    def add_sample(self, value):
+        """Adds a sample to the current Count Aggregation Data and adds 1 to
+        the count data"""
+        self._count_data = self._count_data + 1
+
+    @property
+    def count_data(self):
+        """The current count data"""
+        return self._count_data
+
+
+class DistributionAggregationData(BaseAggregationData):
+    """Distribution Aggregation Data refers to the distribution stats of
+    aggregated data
+
+    :type mean_data: float
+    :param mean_data: the mean value of the distribution
+
+    :type count_data: int
+    :param count_data: the count value of the distribution
+
+    :type min_: double
+    :param min_: the minimum value of the distribution
+
+    :type max_: double
+    :param max_: the maximum value of the distribution
+
+    :type sum_of_sqd_deviations: float
+    :param sum_of_sqd_deviations: the sum of the sqd deviations from the mean
+
+    :type counts_per_bucket: list(int)
+    :param counts_per_bucket: the number of occurrences per bucket
+
+    :type bounds: list(float)
+    :param bounds: the histogram distribution of the values
+
+    """
+    def __init__(self,
+                 mean_data,
+                 count_data,
+                 min_,
+                 max_,
+                 sum_of_sqd_deviations,
+                 counts_per_bucket,
+                 bounds):
+        super(DistributionAggregationData, self).__init__(mean_data)
+        self._mean_data = mean_data
+        self._count_data = count_data
+        self._min = min_
+        self._max = max_
+        self._sum_of_sqd_deviations = sum_of_sqd_deviations
+        self._counts_per_bucket = counts_per_bucket
+        self._bounds = bucket_boundaries.BucketBoundaries(
+                                            boundaries=bounds).boundaries
+
+    @property
+    def mean_data(self):
+        """The current mean data"""
+        return self._mean_data
+
+    @property
+    def count_data(self):
+        """The current count data"""
+        return self._count_data
+
+    @property
+    def min(self):
+        """The current min value"""
+        return self._min
+
+    @property
+    def max(self):
+        """The current max value"""
+        return self._max
+
+    @property
+    def sum_of_sqd_deviations(self):
+        """The current sum of squared deviations from the mean"""
+        return self._sum_of_sqd_deviations
+
+    @property
+    def counts_per_bucket(self):
+        """The current counts per bucket for the distribution"""
+        return self._counts_per_bucket
+
+    @property
+    def bounds(self):
+        """The current bounds for the distribution"""
+        return self._bounds
+
+    @property
+    def sum(self):
+        """The sum of the current distribution"""
+        return self._mean_data * self._count_data
+
+    @property
+    def variance(self):
+        """The variance of the current distribution"""
+        if self._count_data <= 1:
+            return 0
+        return self.sum_of_sqd_deviations / (self._count_data - 1)
+
+    def add_sample(self, value):
+        """Adding a sample to Distribution Aggregation Data"""
+        if value < self.min:
+            self._min = value
+        if value > self.max:
+            self._max = value
+        self._count_data += 1
+        self.increment_bucket_count(value)
+
+        if self.count_data == 1:
+            self._mean_data = value
+            return
+
+        old_mean = self._mean_data
+        self._mean_data = self._mean_data + (
+                          (value - self._mean_data) / self._count_data)
+        self._sum_of_sqd_deviations = self._sum_of_sqd_deviations + (
+                                      (value - old_mean) *
+                                      (value - self._mean_data))
+
+    def increment_bucket_count(self, value):
+        """Increment the bucket count based on a given value from the user"""
+        if len(self._bounds) == 0:
+            self._counts_per_bucket[0] += 1
+            return
+
+        i = 0
+        for b in self._bounds:
+            if value < b:
+                self._counts_per_bucket[i] += 1
+                return
+            i += 1
+
+        self._counts_per_bucket[(len(self._bounds))-1] += 1
diff --git a/opencensus/stats/bucket_boundaries.py b/opencensus/stats/bucket_boundaries.py
new file mode 100644
index 00000000..9028ef2
--- /dev/null
+++ b/opencensus/stats/bucket_boundaries.py
@@ -0,0 +1,41 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class BucketBoundaries(object):
+    """The bucket boundaries for a histogram
+
+    :type boundaries: list(float)
+    :param boundaries: boundaries for the buckets in the underlying histogram
+
+    """
+    def __init__(self, boundaries=None):
+        self._boundaries = list(boundaries or [])
+
+    @property
+    def boundaries(self):
+        """the current boundaries"""
+        return self._boundaries
+
+    def is_valid_boundaries(self, boundaries):
+        """checks if the boundaries are in ascending order"""
+        if boundaries is not None:
+            min_ = boundaries[0]
+            for value in boundaries:
+                if value < min_:
+                    return False
+                else:
+                    min_ = value
+            return True
+        return False
diff --git a/opencensus/stats/measure.py b/opencensus/stats/measure.py
new file mode 100644
index 00000000..2c8ca50
--- /dev/null
+++ b/opencensus/stats/measure.py
@@ -0,0 +1,60 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class BaseMeasure(object):
+    """ A measure is the type of metric that is being recorded with
+    a name, description, and unit
+
+    :type name: str
+    :param name: string representing the name of the measure
+
+    :type description: str
+    :param description: a string representing the description of the measure
+
+    :type unit: str
+    :param unit: the units in which the measure values are measured
+
+    """
+    def __init__(self, name, description, unit=None):
+        self._name = name
+        self._description = description
+        self._unit = unit
+
+    @property
+    def name(self):
+        """The name of the current measure"""
+        return self._name
+
+    @property
+    def description(self):
+        """The description of the current measure"""
+        return self._description
+
+    @property
+    def unit(self):
+        """The unit of the current measure"""
+        return self._unit
+
+
+class MeasureInt(BaseMeasure):
+    """Creates an Integer Measure"""
+    def __init__(self, name, description, unit=None):
+        super(MeasureInt, self).__init__(name, description, unit)
+
+
+class MeasureFloat(BaseMeasure):
+    """Creates a Float Measure"""
+    def __init__(self, name, description, unit=None):
+        super(MeasureFloat, self).__init__(name, description, unit)
diff --git a/opencensus/stats/measure_to_view_map.py b/opencensus/stats/measure_to_view_map.py
new file mode 100644
index 00000000..96e0361
--- /dev/null
+++ b/opencensus/stats/measure_to_view_map.py
@@ -0,0 +1,95 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from opencensus.stats.view_data import ViewData
+from collections import defaultdict
+import logging
+import copy
+
+
+class MeasureToViewMap(object):
+    """Measure To View Map stores a map from names of Measures to
+    specific View Datas
+
+    """
+    def __init__(self):
+        # stores the one-to-many mapping from Measures to View Datas
+        self._map = defaultdict(list)
+        # stores a map from the registered View names to the Views
+        self._registered_views = {}
+        # stores a map from the registered Measure names to the Measures
+        self._registered_measures = {}
+        # stores the set of the exported views
+        self._exported_views = set()
+
+    @property
+    def exported_views(self):
+        """the current exported views"""
+        return self._exported_views
+
+    def get_view(self, view_name, timestamp):
+        """get the View Data from the given View name"""
+        view = self._registered_views.get(view_name)
+        if view is None:
+            return None
+
+        views = self._map.get(view.measure.name)
+        if views is not None:
+            for view_data in views:
+                if view_data.view.name == view_name:
+                    return copy.deepcopy(view_data)
+
+    def filter_exported_views(self, all_views):
+        """returns the subset of the given view that should be exported"""
+        views = set(all_views)
+        return views
+
+    def register_view(self, view, timestamp):
+        """registers the view's measure name to View Datas given a view"""
+        self._exported_views = None
+        existing_view = self._registered_views.get(view.name)
+        if existing_view is not None:
+            if existing_view == view:
+                # ignore the views that are already registered
+                return
+            else:
+                logging.warning(
+                    "A different view with the same name is already registered"
+                )  # pragma: NO COVER
+        measure = view.measure
+        registered_measure = self._registered_measures.get(measure.name)
+        if registered_measure is not None and registered_measure != measure:
+            logging.warning(
+                "A different measure with the same name is already registered")
+        self._registered_views[measure.name] = view
+        if registered_measure is None:
+            self._registered_measures[measure.name] = measure
+        self._map[view.measure.name].append(ViewData(view=view,
+                                                     start_time=timestamp,
+                                                     end_time=timestamp))
+
+    def record(self, tags, stats, timestamp):
+        """records stats with a set of tags"""
+        for measurement, value in stats.items():
+            measure = measurement.measure
+            if measure != self._registered_measures.get(measure.name):
+                return
+            view_datas = []
+            for key, value in self._map.items():
+                if key == measure.name:
+                    view_datas.append(self._map[key])
+            for view_data in view_datas:
+                view_data.record(context=tags,
+                                 value=view_data.view.measure,
+                                 timestamp=timestamp)
diff --git a/opencensus/stats/measurement.py b/opencensus/stats/measurement.py
new file mode 100644
index 00000000..2e0864f
--- /dev/null
+++ b/opencensus/stats/measurement.py
@@ -0,0 +1,50 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class Measurement(object):
+    """ A measurement is an object with a measure and a value attached to it
+
+    :type measure: :class: '~opencensus.stats.measure.Measure'
+    :param measure: A measure to pass into the measurement
+
+    :type value: int or float
+    :param value: value of the measurement
+
+    """
+    def __init__(self, measure, value):
+        self._measure = measure
+        self._value = value
+
+    @property
+    def value(self):
+        """The value of the current measurement"""
+        return self._value
+
+    @property
+    def measure(self):
+        """The measure of the current measurement"""
+        return self._measure
+
+
+class MeasurementInt(Measurement):
+    """ Creates a new Integer Measurement """
+    def __init__(self, measure, value):
+        super(MeasurementInt, self).__init__(measure, value)
+
+
+class MeasurementFloat(Measurement):
+    """ Creates a new Float Measurement """
+    def __init__(self, measure, value):
+        super(MeasurementFloat, self).__init__(measure, value)
diff --git a/opencensus/stats/measurement_map.py b/opencensus/stats/measurement_map.py
new file mode 100644
index 00000000..8085082
--- /dev/null
+++ b/opencensus/stats/measurement_map.py
@@ -0,0 +1,57 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from datetime import datetime
+
+
+class MeasurementMap(object):
+    """Measurement Map is a map from Measures to measured values
+    to be recorded at the same time
+
+    :type measure_to_view_map: :class: '~opencensus.stats.measure_to_view_map.
+                                        MeasureToViewMap'
+    :param measure_to_view_map: the measure to view map that will store the
+                                recorded stats with tags
+
+    """
+    def __init__(self, measure_to_view_map):
+        self._measurement_map = {}
+        self._measure_to_view_map = measure_to_view_map
+
+    @property
+    def measurement_map(self):
+        """the current measurement map"""
+        return self._measurement_map
+
+    @property
+    def measure_to_view_map(self):
+        """the current measure to view map for the measurement map"""
+        return self._measure_to_view_map
+
+    def measure_int_put(self, measure, value):
+        """associates the measure of type Int with the given value"""
+        self._measurement_map[measure] = value
+
+    def measure_float_put(self, measure, value):
+        """associates the measure of type Float with the given value"""
+        self._measurement_map[measure] = value
+
+    def record(self, tag_map_tags):
+        """records all the measures at the same time with an explicit tag_map
+        """
+        self.measure_to_view_map.MeasureToViewMap.record(
+                tags=tag_map_tags,
+                stats=self.measurement_map,
+                timestamp=datetime.utcnow().isoformat() + 'Z'
+        )
diff --git a/opencensus/stats/stats.py b/opencensus/stats/stats.py
new file mode 100644
index 00000000..c1509e3
--- /dev/null
+++ b/opencensus/stats/stats.py
@@ -0,0 +1,35 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from opencensus.stats.stats_recorder import StatsRecorder
+from opencensus.stats.view_manager import ViewManager
+
+
+class Stats(object):
+    """Stats defines a View Manager and a Stats Recorder in order for the
+    collection of Stats
+    """
+    def __init__(self):
+        self._stats_recorder = StatsRecorder()
+        self._view_manager = ViewManager()
+
+    @property
+    def stats_recorder(self):
+        """the current stats recorder for Stats"""
+        return self._stats_recorder
+
+    @property
+    def view_manager(self):
+        """the current view manager for Stats"""
+        return self._view_manager
diff --git a/opencensus/stats/stats_recorder.py b/opencensus/stats/stats_recorder.py
new file mode 100644
index 00000000..4c11ebe
--- /dev/null
+++ b/opencensus/stats/stats_recorder.py
@@ -0,0 +1,33 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from opencensus.stats.measurement_map import MeasurementMap
+
+
+class StatsRecorder(object):
+    """Stats Recorder provides methods to record stats against tags
+
+    :type measure_to_view_map: :class: '~opencensus.stats.measure_to_view_map.
+                                        MeasureToViewMap'
+    :param measure_to_view_map: the measure to view map for the stats recorder
+
+    """
+    def __init__(self, measure_to_view_map=None):
+        self.measure_to_view_map = measure_to_view_map or {}
+
+    def new_measurement_map(self):
+        """Creates a new MeasurementMap in order to record stats
+        :returns a MeasurementMap for recording multiple measurements
+        """
+        return MeasurementMap(self.measure_to_view_map)
diff --git a/opencensus/stats/view.py b/opencensus/stats/view.py
new file mode 100644
index 00000000..e23dd3b
--- /dev/null
+++ b/opencensus/stats/view.py
@@ -0,0 +1,66 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class View(object):
+    """A view defines a specific aggregation and a set of tag keys
+
+    :type name: str
+    :param name: name of the view
+
+    :type description: str
+    :param description: description of the view
+
+    :type columns: (:class: '~opencensus.tags.tag_key.TagKey')
+    :param columns: the columns that the tag keys will aggregate on for this
+                    view
+
+    :type measure: :class: '~opencensus.stats.measure.Measure'
+    :param measure: the measure to be aggregated by the view
+
+    :type aggregation: :class: '~opencensus.stats.aggregation.BaseAggregation'
+    :param aggregation: the aggregation the view will support
+
+    """
+    def __init__(self, name, description, columns, measure, aggregation):
+        self._name = name
+        self._description = description
+        self._columns = columns
+        self._measure = measure
+        self._aggregation = aggregation
+
+    @property
+    def name(self):
+        """the name of the current view"""
+        return self._name
+
+    @property
+    def description(self):
+        """the description of the current view"""
+        return self._description
+
+    @property
+    def columns(self):
+        """the columns of the current view"""
+        return self._columns
+
+    @property
+    def measure(self):
+        """the measure of the current view"""
+        return self._measure
+
+    @property
+    def aggregation(self):
+        """the aggregation of the current view"""
+        return self._aggregation
diff --git a/opencensus/stats/view_data.py b/opencensus/stats/view_data.py
new file mode 100644
index 00000000..7975e6c
--- /dev/null
+++ b/opencensus/stats/view_data.py
@@ -0,0 +1,104 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from datetime import datetime
+
+
+class ViewData(object):
+    """View Data is the aggregated data for a particular view
+
+    :type view:
+    :param view: The view associated with this view data
+
+    :type start_time: datetime
+    :param start_time: the start time for this view data
+
+    :type end_time: datetime
+    :param end_time: the end time for this view data
+
+    """
+    def __init__(self,
+                 view,
+                 start_time,
+                 end_time):
+        self._view = view
+        self._start_time = start_time
+        self._end_time = end_time
+        self._tag_value_aggregation_map = {}
+        self._tag_map = {}
+
+    @property
+    def view(self):
+        """the current view in the view data"""
+        return self._view
+
+    @property
+    def start_time(self):
+        """the current start time in the view data"""
+        return self._start_time
+
+    @property
+    def end_time(self):
+        """the current end time in the view data"""
+        return self._end_time
+
+    @property
+    def tag_value_aggregation_map(self):
+        """the current tag value aggregation map in the view data"""
+        return self._tag_value_aggregation_map
+
+    @property
+    def tag_map(self):
+        return self._tag_map
+
+    def start(self):
+        """sets the start time for the view data"""
+        self._start_time = datetime.utcnow().isoformat() + 'Z'
+
+    def end(self):
+        """sets the end time for the view data"""
+        self._end_time = datetime.utcnow().isoformat() + 'Z'
+
+    def get_tag_map(self, context):
+        """function to return the tag map based on the context"""
+        if context.items() <= self.tag_map.items():
+            return self.tag_map
+        else:
+            tags = self.tag_map
+            for tag_key, tag_value in context.items():
+                tags[tag_key] = tag_value
+            return tags
+
+    def get_tag_values(self, tags, columns):
+        """function to get the tag values from tags and columns"""
+        tag_values = []
+        i = 0
+        while i < len(columns):
+            tag_key = columns[i]
+            if tag_key in tags:
+                tag_values.append(tags.get(tag_key))
+            else:
+                tag_values.append(None)
+            i += 1
+        return tag_values
+
+    def record(self, context, value, timestamp):
+        """records the view data against context"""
+        tag_values = self.get_tag_values(tags=self.get_tag_map(context),
+                                         columns=self.view.columns)
+        tuple_vals = tuple(tag_values)
+        for val in tuple_vals:
+            if val not in self.tag_value_aggregation_map:
+                self.tag_value_aggregation_map[val] = self.view.aggregation
+            self.tag_value_aggregation_map.get(val).add(value)
diff --git a/opencensus/stats/view_manager.py b/opencensus/stats/view_manager.py
new file mode 100644
index 00000000..7e104d7
--- /dev/null
+++ b/opencensus/stats/view_manager.py
@@ -0,0 +1,43 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from opencensus.stats.measure_to_view_map import MeasureToViewMap
+from datetime import datetime
+
+
+class ViewManager(object):
+    """View Manager allows the registering of Views for collecting stats
+    and receiving stats data as View Data"""
+    def __init__(self):
+        self.time = datetime.utcnow().isoformat() + 'Z'
+        self._measure_view_map = MeasureToViewMap()
+
+    @property
+    def measure_to_view_map(self):
+        """the current measure to view map for the View Manager"""
+        return self._measure_view_map
+
+    def register_view(self, view):
+        """registers the given view"""
+        self.measure_to_view_map.register_view(view=view, timestamp=self.time)
+
+    def get_view(self, view_name):
+        """gets the view given the view name """
+        return self.measure_to_view_map.get_view(view_name=view_name,
+                                                 timestamp=self.time)
+
+    def get_all_exported_views(self):
+        """returns all of the exported views for the current measure to view
+        map"""
+        return self.measure_to_view_map.exported_views
diff --git a/opencensus/tags/__init__.py b/opencensus/tags/__init__.py
new file mode 100644
index 00000000..ebe2afa
--- /dev/null
+++ b/opencensus/tags/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/opencensus/tags/tag.py b/opencensus/tags/tag.py
new file mode 100644
index 00000000..f5ef575
--- /dev/null
+++ b/opencensus/tags/tag.py
@@ -0,0 +1,41 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from opencensus.tags import tag_key
+from opencensus.tags import tag_value
+
+
+class Tag(object):
+    """ A tag, in the format [KEY]:[VALUE].
+
+    :type key: :class: '~opencensus.tags.tag_key.TagKey'
+    :param key: Key in the tag
+
+    :type value: :class: 'opencensus.tags.tag_value.TagValue'
+    :param value: Value of the key in the tag.
+
+    """
+    def __init__(self, key, value):
+        self._key = tag_key.TagKey(key)
+        self._value = tag_value.TagValue(value)
+
+    @property
+    def key(self):
+        """The key of the current tag"""
+        return self._key
+
+    @property
+    def value(self):
+        """The value of the key in the current tag"""
+        return self._value
diff --git a/opencensus/tags/tag_key.py b/opencensus/tags/tag_key.py
new file mode 100644
index 00000000..35cac4f
--- /dev/null
+++ b/opencensus/tags/tag_key.py
@@ -0,0 +1,47 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class TagKey(object):
+    """ A tag key with a property name
+
+    :type name: str
+    :param name: The name of the key
+
+    """
+    def __init__(self, name):
+        self._name = name
+
+    @property
+    def name(self):
+        """The name of the current key"""
+        return self._name
+
+    def is_valid_name(self, name):
+        """Checks if the name of the key is valid
+
+        :type name: str
+        :param name: name to check
+
+        :rtype: bool
+        :returns: True if it valid, else returns False
+        """
+        if (len(name) > 0) and (len(name) <= 255):
+            if (all(ord(char) < 126 for char in name) and
+                    all(ord(char) > 32 for char in name)):
+                return True
+            else:
+                return False
+        else:
+            return False
diff --git a/opencensus/tags/tag_map.py b/opencensus/tags/tag_map.py
new file mode 100644
index 00000000..07c1e94
--- /dev/null
+++ b/opencensus/tags/tag_map.py
@@ -0,0 +1,102 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class TagMap(object):
+    """ A tag map is a map of tags from key to value
+
+    :type tags: list(:class: '~opencensus.tags.tag.Tag')
+    :param tags: a list of tags
+
+    """
+    def __init__(self, tags=None):
+        self._map = {}
+        if tags is not None:
+            self.tags = tags
+            for tag in self.tags:
+                for tag_key, tag_value in tag.items():
+                    self._map[tag_key] = tag_value
+
+        else:
+            self.tags = {}
+
+    @property
+    def map(self):
+        """The current map of tags"""
+        return self._map
+
+    def insert(self, key, value):
+        """Inserts a key and value in the map if the map does not already
+        contain the key.
+
+        :type key: :class: '~opencensus.tags.tag_key.TagKey'
+        :param key: a tag key to insert into the map
+
+        :type value: :class: '~opencensus.tags.tag_value.TagValue'
+        :param value: a tag value that is associated with the tag key and
+        the value to insert into the tag map
+
+        """
+        if key not in self._map:
+            self._map[key] = value
+
+    def delete(self, key):
+        """ Deletes a tag from the map if the key is in the map
+
+        :type key: str
+        :param key: A string representing a possible tag key
+
+        :returns: the value of the key in the dictionary if it is in there,
+                  or None if it is not.
+        """
+        self._map.pop(key, None)
+
+    def update(self, key, value):
+        """ Updates the map by updating the value of a key
+
+        :type key: :class: '~opencensus.tags.tag_key.TagKey'
+        :param key: A tag key to be updated
+
+        :type value: str
+        :param value: The value to update the key to in the map
+
+        """
+        if key in self._map:
+            self._map[key] = value
+
+    def tag_key_exists(self, key):
+        """ Checking if the tag key exists in the map
+
+        :type key: str
+        :param key: A string to check to see if that is a key in the map
+
+        :returns: True if the key is in map, False is it is not
+
+        """
+        return key in self._map
+
+    def get_value(self, key):
+        """ Gets the value of the key passed in if the key exists in the map
+
+        :type key: str
+        :param key: A string representing a key to get the value of in the map
+
+        :returns: A KeyError if the value is None, else returns the value
+
+        """
+        value = self._map.get(key, None)
+        if value is None:
+            raise KeyError('Key is not in map.')
+
+        return value
diff --git a/opencensus/tags/tag_value.py b/opencensus/tags/tag_value.py
new file mode 100644
index 00000000..db79786
--- /dev/null
+++ b/opencensus/tags/tag_value.py
@@ -0,0 +1,48 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class TagValue(object):
+    """ The value of a tag
+
+    :type value: str
+    :param value: A string representing the value of a key in a tag
+
+    """
+    def __init__(self, value):
+        self._value = value
+
+    @property
+    def value(self):
+        """The current value"""
+        return self._value
+
+    def is_valid_value(self, value):
+        """ Checks if the value if valid
+
+        :type value: str
+        :param value: the value to be checked
+
+        :rtype: bool
+        :returns: True if valid, if not, False.
+
+        """
+        if len(value) <= 255:
+            if (all(ord(char) < 126 for char in value) and
+                    all(ord(char) > 32 for char in value)):
+                return True
+            else:
+                return False
+        else:
+            return False
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..3951118
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,2 @@
+[tox]
+envlist = py27


Test patch:
diff --git a/requirements-test.txt b/requirements-test.txt
index 650ea14..78068ce 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,5 +1,6 @@
 Django==1.11.7
 Flask==0.12.2
+google-cloud-monitoring==0.29.0
 google-cloud-trace==0.17.0
 grpcio==1.8.3
 mock==2.0.0
diff --git a/tests/unit/stats/test_aggregation.py b/tests/unit/stats/test_aggregation.py
new file mode 100644
index 00000000..7a6db45
--- /dev/null
+++ b/tests/unit/stats/test_aggregation.py
@@ -0,0 +1,87 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import mock
+from opencensus.stats import aggregation as aggregation_module
+
+
+class TestBaseAggregation(unittest.TestCase):
+
+    def test_constructor_defaults(self):
+        base_aggregation = aggregation_module.BaseAggregation()
+
+        self.assertEqual("none", base_aggregation.aggregation_type)
+        self.assertEqual([], base_aggregation.buckets)
+
+    def test_constructor_explicit(self):
+
+        buckets = ["test"]
+        base_aggregation = aggregation_module.BaseAggregation(buckets=buckets)
+
+        self.assertEqual("none", base_aggregation.aggregation_type)
+        self.assertEqual(["test"], base_aggregation.buckets)
+
+
+class TestSumAggregation(unittest.TestCase):
+
+    def test_constructor_defaults(self):
+        sum_aggregation = aggregation_module.SumAggregation()
+
+        self.assertEqual(0, sum_aggregation.sum.sum_data)
+        self.assertEqual("sum", sum_aggregation.aggregation_type)
+
+    def test_constructor_explicit(self):
+        sum = 1
+
+        sum_aggregation = aggregation_module.SumAggregation(sum=sum)
+
+        self.assertEqual(1, sum_aggregation.sum.sum_data)
+        self.assertEqual("sum", sum_aggregation.aggregation_type)
+
+
+class TestCountAggregation(unittest.TestCase):
+
+    def test_constructor_defaults(self):
+        count_aggregation = aggregation_module.CountAggregation()
+
+        self.assertEqual(0, count_aggregation.count.count_data)
+        self.assertEqual("count", count_aggregation.aggregation_type)
+
+    def test_constructor_explicit(self):
+        count = 4
+
+        count_aggregation = aggregation_module.CountAggregation(count=count)
+
+        self.assertEqual(4, count_aggregation.count.count_data)
+        self.assertEqual("count", count_aggregation.aggregation_type)
+
+
+class TestDistributionAggregation(unittest.TestCase):
+
+    def test_constructor_defaults(self):
+        distribution_aggregation = aggregation_module.DistributionAggregation()
+
+        self.assertEqual([], distribution_aggregation.boundaries.boundaries)
+        self.assertEqual({}, distribution_aggregation.distribution)
+        self.assertEqual("distribution", distribution_aggregation.aggregation_type)
+
+    def test_constructor_explicit(self):
+        boundaries = ["test"]
+        distribution = {1: "test"}
+        distribution_aggregation = aggregation_module.DistributionAggregation(boundaries=boundaries, distribution=distribution)
+
+        self.assertEqual(["test"], distribution_aggregation.boundaries.boundaries)
+        self.assertEqual({1: "test"}, distribution_aggregation.distribution)
+        self.assertEqual("distribution", distribution_aggregation.aggregation_type)
diff --git a/tests/unit/stats/test_aggregation_data.py b/tests/unit/stats/test_aggregation_data.py
new file mode 100644
index 00000000..20fdbf9
--- /dev/null
+++ b/tests/unit/stats/test_aggregation_data.py
@@ -0,0 +1,233 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import mock
+from opencensus.stats import aggregation_data as aggregation_data_module
+
+
+class TestBaseAggregationData(unittest.TestCase):
+
+    def test_constructor(self):
+        aggregation_data = 0
+        base_aggregation_data = aggregation_data_module.BaseAggregationData(
+            aggregation_data=aggregation_data)
+
+        self.assertEqual(0, base_aggregation_data.aggregation_data)
+
+
+class TestSumAggregationData(unittest.TestCase):
+
+    def test_constructor(self):
+        sum_data = 1
+        sum_aggregation_data = aggregation_data_module.SumAggregationDataFloat(
+            sum_data=sum_data)
+
+        self.assertEqual(1, sum_aggregation_data.sum_data)
+
+    def test_add_sample(self):
+        sum_data = 1
+        value = 3
+        sum_aggregation_data = aggregation_data_module.SumAggregationDataFloat(
+            sum_data=sum_data)
+        sum_aggregation_data.add_sample(value=value)
+
+        self.assertEqual(4, sum_aggregation_data.sum_data)
+
+
+class TestCountAggregationData(unittest.TestCase):
+
+    def test_constructor(self):
+        count_data = 0
+        count_aggregation_data = aggregation_data_module.CountAggregationData(
+            count_data=count_data)
+
+        self.assertEqual(0, count_aggregation_data.count_data)
+
+    def test_add_sample(self):
+        count_data = 0
+        count_aggregation_data = aggregation_data_module.CountAggregationData(
+            count_data=count_data)
+        count_aggregation_data.add_sample(10)
+
+        self.assertEqual(1, count_aggregation_data.count_data)
+
+
+class TestDistributionAggregationData(unittest.TestCase):
+
+    def test_constructor(self):
+        mean_data = 1
+        count_data = 0
+        _min = 0
+        _max = 1
+        sum_of_sqd_deviations = mock.Mock()
+        counts_per_bucket = [1, 1, 1]
+        bounds = [0, 1/2, 1]
+
+        dist_agg_data = aggregation_data_module.DistributionAggregationData(
+                mean_data=mean_data,
+                count_data=count_data,
+                min_= _min,
+                max_ = _max,
+                sum_of_sqd_deviations=sum_of_sqd_deviations,
+                counts_per_bucket=counts_per_bucket,
+                bounds=bounds
+            )
+
+        self.assertEqual(1, dist_agg_data.mean_data)
+        self.assertEqual(0, dist_agg_data.count_data)
+        self.assertEqual(0, dist_agg_data.min)
+        self.assertEqual(1, dist_agg_data.max)
+        self.assertEqual(sum_of_sqd_deviations,
+                         dist_agg_data.sum_of_sqd_deviations)
+        self.assertEqual([1, 1, 1], dist_agg_data.counts_per_bucket)
+        self.assertEqual([0, 1/2, 1], dist_agg_data.bounds)
+
+        self.assertIsNotNone(dist_agg_data.sum)
+        self.assertEqual(0, dist_agg_data.variance)
+
+    def test_variance(self):
+        mean_data = mock.Mock()
+        count_data = 0
+        _min = mock.Mock()
+        _max = mock.Mock()
+        sum_of_sqd_deviations = mock.Mock()
+        counts_per_bucket = [1, 1, 1]
+        bounds = [0, 1/2, 1]
+        dist_agg_data = aggregation_data_module.DistributionAggregationData(
+            mean_data=mean_data,
+            count_data=count_data,
+            min_= _min,
+            max_ = _max,
+            sum_of_sqd_deviations=sum_of_sqd_deviations,
+            counts_per_bucket=counts_per_bucket,
+            bounds=bounds
+        )
+        self.assertEqual(0, dist_agg_data.variance)
+
+        count_data = 2
+        sum_of_sqd_deviations = 2
+        dist_agg_data = aggregation_data_module.DistributionAggregationData(
+            mean_data=mean_data,
+            count_data=count_data,
+            min_= _min,
+            max_ = _max,
+            sum_of_sqd_deviations=sum_of_sqd_deviations,
+            counts_per_bucket=counts_per_bucket,
+            bounds=bounds
+        )
+        self.assertEqual(2.0, dist_agg_data.variance)
+
+    def test_add_sample(self):
+        mean_data = 1.0
+        count_data = 0
+        _min = 0
+        _max = 1
+        sum_of_sqd_deviations = 2
+        counts_per_bucket = [1, 1, 1, 1]
+        bounds = [0, 0.5, 1, 1.5]
+
+        value = 3
+
+        dist_agg_data = aggregation_data_module.DistributionAggregationData(
+            mean_data=mean_data,
+            count_data=count_data,
+            min_= _min,
+            max_ = _max,
+            sum_of_sqd_deviations=sum_of_sqd_deviations,
+            counts_per_bucket=counts_per_bucket,
+            bounds=bounds
+        )
+
+        dist_agg_data.add_sample(value=value)
+        self.assertEqual(0, dist_agg_data.min)
+        self.assertEqual(3, dist_agg_data.max)
+        self.assertEqual(1, dist_agg_data.count_data)
+        self.assertEqual(value, dist_agg_data.mean_data)
+
+        count_data = 1
+        dist_agg_data = aggregation_data_module.DistributionAggregationData(
+            mean_data=mean_data,
+            count_data=count_data,
+            min_= _min,
+            max_ = _max,
+            sum_of_sqd_deviations=sum_of_sqd_deviations,
+            counts_per_bucket=counts_per_bucket,
+            bounds=bounds
+        )
+
+        dist_agg_data.add_sample(value=value)
+        self.assertEqual(2, dist_agg_data.count_data)
+        self.assertEqual(2.0, dist_agg_data.mean_data)
+        self.assertEqual(4.0, dist_agg_data.sum_of_sqd_deviations)
+        self.assertIsNot(0, dist_agg_data.count_data)
+
+        value_2 = -1
+        dist_agg_data.add_sample(value=value_2)
+        self.assertEqual(value_2, dist_agg_data.min)
+
+    def test_increment_bucket_count(self):
+        mean_data = mock.Mock()
+        count_data = mock.Mock()
+        _min = 0
+        _max = 1
+        sum_of_sqd_deviations = mock.Mock()
+        counts_per_bucket = [0]
+        bounds = []
+
+        value = 1
+
+        dist_agg_data = aggregation_data_module.DistributionAggregationData(
+            mean_data=mean_data,
+            count_data=count_data,
+            min_= _min,
+            max_ = _max,
+            sum_of_sqd_deviations=sum_of_sqd_deviations,
+            counts_per_bucket=counts_per_bucket,
+            bounds=bounds
+        )
+
+        dist_agg_data.increment_bucket_count(value=value)
+        self.assertEqual([1], dist_agg_data.counts_per_bucket)
+
+        counts_per_bucket = [1, 1]
+        bounds = [1/4, 3/2]
+
+        dist_agg_data = aggregation_data_module.DistributionAggregationData(
+            mean_data=mean_data,
+            count_data=count_data,
+            min_= _min,
+            max_ = _max,
+            sum_of_sqd_deviations=sum_of_sqd_deviations,
+            counts_per_bucket=counts_per_bucket,
+            bounds=bounds
+        )
+
+        dist_agg_data.increment_bucket_count(value=value)
+        self.assertEqual([1, 2], dist_agg_data.counts_per_bucket)
+
+        bounds = [1/4, 1/2]
+
+        dist_agg_data = aggregation_data_module.DistributionAggregationData(
+            mean_data=mean_data,
+            count_data=count_data,
+            min_= _min,
+            max_ = _max,
+            sum_of_sqd_deviations=sum_of_sqd_deviations,
+            counts_per_bucket=counts_per_bucket,
+            bounds=bounds
+        )
+
+        dist_agg_data.increment_bucket_count(value=value)
+        self.assertEqual([1, 3], dist_agg_data.counts_per_bucket)
diff --git a/tests/unit/stats/test_bucket_boundaries.py b/tests/unit/stats/test_bucket_boundaries.py
new file mode 100644
index 00000000..e24f806
--- /dev/null
+++ b/tests/unit/stats/test_bucket_boundaries.py
@@ -0,0 +1,47 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import mock
+from opencensus.stats import bucket_boundaries as bucket_boundaries_module
+
+
+class TestBucketBoundaries(unittest.TestCase):
+
+    def test_constructor_defaults(self):
+        bucket_boundaries = bucket_boundaries_module.BucketBoundaries()
+        self.assertEqual([], bucket_boundaries.boundaries)
+
+    def test_constructor_explicit(self):
+        boundaries = [1/4]
+        bucket_boundaries = bucket_boundaries_module.BucketBoundaries(boundaries=boundaries)
+        self.assertTrue(bucket_boundaries.is_valid_boundaries(boundaries))
+        self.assertEqual(boundaries, bucket_boundaries.boundaries)
+
+    def test_is_valid_boundaries(self):
+        boundaries = [0, 1/4, 1/2]
+        bucket_boundaries = bucket_boundaries_module.BucketBoundaries(boundaries=boundaries)
+        self.assertTrue(bucket_boundaries.is_valid_boundaries(boundaries=boundaries))
+
+        boundaries = [2, 1, 0]
+        bucket_boundaries = bucket_boundaries_module.BucketBoundaries(
+            boundaries=boundaries)
+        self.assertFalse(bucket_boundaries.is_valid_boundaries(
+            boundaries=boundaries))
+
+        boundaries = None
+        bucket_boundaries = bucket_boundaries_module.BucketBoundaries(
+            boundaries=boundaries
+        )
+        self.assertFalse(bucket_boundaries.is_valid_boundaries(boundaries=boundaries))
diff --git a/tests/unit/stats/test_measure.py b/tests/unit/stats/test_measure.py
new file mode 100644
index 00000000..6645cde
--- /dev/null
+++ b/tests/unit/stats/test_measure.py
@@ -0,0 +1,83 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import mock
+from opencensus.stats import measure as measure_module
+
+
+class TestBaseMeasure(unittest.TestCase):
+
+    def test_constructor_defaults(self):
+        name = "testName"
+        description = "testMeasure"
+
+        measure = measure_module.BaseMeasure(name=name, description=description)
+
+        self.assertEqual(None, measure.unit)
+
+    def test_constructor_explicit(self):
+        name = "testName"
+        description = "testMeasure"
+        unit = "testUnit"
+
+        measure = measure_module.BaseMeasure(name=name, description=description, unit=unit)
+
+        self.assertEqual("testName", measure.name)
+        self.assertEqual("testMeasure", measure.description)
+        self.assertEqual("testUnit", measure.unit)
+
+
+class TestMeasureInt(unittest.TestCase):
+
+    def test_constructor_defaults(self):
+        name = "testName"
+        description = "testMeasure"
+
+        measure = measure_module.MeasureInt(name=name, description=description)
+
+        self.assertEqual(None, measure.unit)
+
+    def test_constructor_explicit(self):
+        name = "testName"
+        description = "testMeasure"
+        unit = "testUnit"
+
+        measure = measure_module.MeasureInt(name=name, description=description, unit=unit)
+
+        self.assertEqual("testName", measure.name)
+        self.assertEqual("testMeasure", measure.description)
+        self.assertEqual("testUnit", measure.unit)
+
+
+class TestMeasureFloat(unittest.TestCase):
+
+    def test_constructor_defaults(self):
+        name = "testName"
+        description = "testMeasure"
+
+        measure = measure_module.MeasureFloat(name=name, description=description)
+
+        self.assertEqual(None, measure.unit)
+
+    def test_constructor_explicit(self):
+        name = "testName"
+        description = "testMeasure"
+        unit = "testUnit"
+
+        measure = measure_module.MeasureFloat(name=name, description=description, unit=unit)
+
+        self.assertEqual("testName", measure.name)
+        self.assertEqual("testMeasure", measure.description)
+        self.assertEqual("testUnit", measure.unit)
diff --git a/tests/unit/stats/test_measure_to_view_map.py b/tests/unit/stats/test_measure_to_view_map.py
new file mode 100644
index 00000000..8d2f227
--- /dev/null
+++ b/tests/unit/stats/test_measure_to_view_map.py
@@ -0,0 +1,194 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import mock
+import logging
+from datetime import datetime
+from opencensus.stats.view import View
+from opencensus.stats.view_data import ViewData
+from opencensus.stats.measurement import Measurement
+from opencensus.stats.measure import BaseMeasure
+from opencensus.stats import measure_to_view_map as measure_to_view_map_module
+
+
+class TestMeasureToViewMap(unittest.TestCase):
+
+    @staticmethod
+    def _get_target_class():
+        return measure_to_view_map_module.MeasureToViewMap
+
+    def _make_one(self, *args, **kw):
+        return self._get_target_class()(*args, **kw)
+
+    def test_constructor(self):
+        measure_to_view_map = measure_to_view_map_module.MeasureToViewMap()
+
+        self.assertEqual({}, measure_to_view_map._map)
+        self.assertEqual({}, measure_to_view_map._registered_views)
+        self.assertEqual({}, measure_to_view_map._registered_measures)
+        self.assertEqual(set(), measure_to_view_map.exported_views)
+
+    def test_get_view(self):
+        name = "testView"
+        description = "testDescription"
+        columns = mock.Mock()
+        measure = mock.Mock()
+        aggregation = mock.Mock()
+        view = View(name=name, description=description, columns=columns, measure=measure, aggregation=aggregation)
+        timestamp = mock.Mock()
+        measure_to_view_map = measure_to_view_map_module.MeasureToViewMap()
+
+        measure_to_view_map._registered_views = {}
+        no_registered_views = measure_to_view_map.get_view(view_name=name, timestamp=timestamp)
+        self.assertEqual(None, no_registered_views)
+
+        measure_to_view_map._registered_views = {name: view}
+        measure_to_view_map._map = {view.measure.name: [ViewData(view=view, start_time=timestamp, end_time=timestamp)]}
+        print(measure_to_view_map._map)
+        view_data = measure_to_view_map.get_view(view_name=name, timestamp=timestamp)
+        self.assertIsNotNone(view_data)
+
+        measure_to_view_map._map = {}
+        view_data = measure_to_view_map.get_view(view_name=name,
+                                                 timestamp=timestamp)
+        self.assertIsNone(view_data)
+
+        measure_to_view_map._map = {view.measure.name: [ViewData(
+            view=mock.Mock(), start_time=timestamp, end_time=timestamp)]}
+        view_data = measure_to_view_map.get_view(view_name=name,
+                                                 timestamp=timestamp)
+        self.assertIsNone(view_data)
+
+    def test_filter_exported_views(self):
+        test_view_1_name = "testView1"
+        description = "testDescription"
+        columns = mock.Mock()
+        measure = mock.Mock()
+        aggregation = mock.Mock()
+        test_view_1 = View(name=test_view_1_name, description=description, columns=columns, measure=measure, aggregation=aggregation)
+        print("test view 1", test_view_1)
+
+        test_view_2_name = "testView2"
+        test_view_2 = View(name=test_view_2_name, description=description, columns=columns, measure=measure, aggregation=aggregation)
+        print("test view 2", test_view_2)
+        all_the_views = {test_view_1, test_view_2}
+        print("all the views", all_the_views)
+        measure_to_view_map = measure_to_view_map_module.MeasureToViewMap()
+        views = measure_to_view_map.filter_exported_views(all_views=all_the_views)
+        print("filtered views", views)
+        self.assertEqual(views, all_the_views)
+
+    def test_register_view(self):
+        name = "testView"
+        description = "testDescription"
+        columns = mock.Mock()
+        measure = mock.Mock()
+        aggregation = mock.Mock()
+        view = View(name=name, description=description, columns=columns, measure=measure, aggregation=aggregation)
+        timestamp = mock.Mock()
+        measure_to_view_map = measure_to_view_map_module.MeasureToViewMap()
+
+        measure_to_view_map._registered_views = {}
+        measure_to_view_map._registered_measures = {}
+        measure_to_view_map.register_view(view=view, timestamp=timestamp)
+        self.assertIsNone(measure_to_view_map.exported_views)
+        self.assertEqual(measure_to_view_map._registered_views[measure.name], view)
+        self.assertEqual(measure_to_view_map._registered_measures[measure.name], measure)
+        self.assertIsNotNone(measure_to_view_map._map[view.measure.name])
+
+        test_measure = mock.Mock()
+        measure_to_view_map._registered_measures = {measure.name: test_measure}
+        test_with_registered_measures = measure_to_view_map.register_view(view=view, timestamp=timestamp)
+        self.assertIsNone(test_with_registered_measures)
+        self.assertIsNotNone(measure_to_view_map._map[view.measure.name])
+
+        measure_to_view_map._registered_measures = {measure.name: None}
+        self.assertIsNone(measure_to_view_map._registered_measures.get(
+            measure.name))
+        measure_to_view_map.register_view(view=view, timestamp=timestamp)
+        self.assertEqual(measure,
+                         measure_to_view_map._registered_measures[measure.name]
+                         )
+        self.assertIsNotNone(measure_to_view_map._map[view.measure.name])
+
+        measure_to_view_map._registered_views = {name: view}
+        test_result_1 = measure_to_view_map.register_view(view=view, timestamp=timestamp)
+        self.assertIsNone(test_result_1)
+        self.assertIsNotNone(measure_to_view_map._map[view.measure.name])
+
+    def test_record(self):
+        measure_name = "test_measure"
+        measure_description = "test_description"
+        measure = BaseMeasure(name=measure_name, description=measure_description)
+
+        view_name = "test_view"
+        view_description = "test_description"
+        view_columns = ["testTag1", "testColumn2"]
+        view_measure = measure
+        view_aggregation = mock.Mock()
+        view = View(name=view_name, description=view_description, columns=view_columns, measure=view_measure, aggregation=view_aggregation)
+
+        measure_value = 5
+        value = "testValue"
+        tags = {"testTag1": "testTag1Value"}
+        stats = {Measurement(measure=measure, value=measure_value): value}
+        timestamp = mock.Mock()
+
+        measure_to_view_map = measure_to_view_map_module.MeasureToViewMap()
+        measure_to_view_map._registered_measures = {}
+        record = measure_to_view_map.record(tags=tags, stats=stats, timestamp=timestamp)
+        self.assertNotEqual(measure,
+                            measure_to_view_map._registered_measures.get(
+                                measure.name))
+        self.assertIsNone(record)
+
+        measure_to_view_map._registered_measures = {measure.name: measure}
+        measure_to_view_map._map = {}
+        record = measure_to_view_map.record(tags=tags, stats=stats,
+                                   timestamp=timestamp)
+        self.assertEqual(measure,
+                         measure_to_view_map._registered_measures.get(
+                             measure.name))
+        self.assertIsNone(record)
+
+        measure_to_view_map._map = {measure.name: mock.Mock()}
+        measure_to_view_map.record(tags=tags, stats=stats,
+                                   timestamp=timestamp)
+        self.assertEqual(measure,
+                         measure_to_view_map._registered_measures.get(
+                             measure.name))
+        self.assertTrue(measure.name in measure_to_view_map._map)
+
+        measure_to_view_map._map = {"testing": mock.Mock()}
+        measure_to_view_map.record(tags=tags, stats=stats,
+                                   timestamp=timestamp)
+        self.assertTrue(measure.name not in measure_to_view_map._map)
+
+        measure_to_view_map_mock = mock.Mock()
+        measure_to_view_map = measure_to_view_map_mock
+        measure_to_view_map._registered_measures = {measure.name: measure}
+        measure_to_view_map._map = mock.Mock()
+        measure_to_view_map.record(tags=mock.Mock(), stats=mock.Mock(), timestamp=mock.Mock())
+        self.assertEqual(measure,
+                         measure_to_view_map._registered_measures.get(
+                             measure.name))
+        self.assertIsNotNone(measure_to_view_map.view_datas)
+        self.assertTrue(measure_to_view_map_mock.record.called)
+
+        tags = {"testTag1": "testTag1Value"}
+        stats = {}
+        measure_to_view_map = measure_to_view_map_module.MeasureToViewMap()
+        record = measure_to_view_map.record(tags=tags, stats=stats, timestamp=timestamp)
+        self.assertIsNone(record)
diff --git a/tests/unit/stats/test_measurement.py b/tests/unit/stats/test_measurement.py
new file mode 100644
index 00000000..afd40fe
--- /dev/null
+++ b/tests/unit/stats/test_measurement.py
@@ -0,0 +1,53 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import mock
+from opencensus.stats import measurement as measurement_module
+
+
+class TestMeasurement(unittest.TestCase):
+
+    def test_constructor(self):
+        measure = "testMeasure"
+        value = 0
+
+        measurement = measurement_module.Measurement(measure=measure, value=value)
+
+        self.assertEqual("testMeasure", measurement.measure)
+        self.assertEqual(0, measurement.value)
+
+
+class TestMeasurementInt(unittest.TestCase):
+
+    def test_constructor(self):
+        measure = "testIntMeasure"
+        value = 10
+
+        measurement = measurement_module.MeasurementInt(measure=measure, value=value)
+
+        self.assertEqual("testIntMeasure", measurement.measure)
+        self.assertEqual(10, measurement.value)
+
+
+class TestMeasurementFloat(unittest.TestCase):
+
+    def test_constructor(self):
+        measure = "testFloatMeasure"
+        value = 10.00
+
+        measurement = measurement_module.MeasurementFloat(measure=measure, value=value)
+
+        self.assertEqual("testFloatMeasure", measurement.measure)
+        self.assertEqual(10.00, measurement.value)
diff --git a/tests/unit/stats/test_measurement_map.py b/tests/unit/stats/test_measurement_map.py
new file mode 100644
index 00000000..b91bd5f
--- /dev/null
+++ b/tests/unit/stats/test_measurement_map.py
@@ -0,0 +1,59 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import mock
+from opencensus.stats import measurement_map as measurement_map_module
+from opencensus.stats.measure_to_view_map import MeasureToViewMap
+
+
+class TestMeasurementMap(unittest.TestCase):
+
+    def test_constructor_explicit(self):
+        measure_to_view_map = {'testMeasure1': 'testVal1'}
+        measurement_map = measurement_map_module.MeasurementMap(
+            measure_to_view_map=measure_to_view_map)
+
+        self.assertEqual(measure_to_view_map,
+                         measurement_map.measure_to_view_map)
+        self.assertEqual({}, measurement_map.measurement_map)
+
+    def test_measure_int_put(self):
+        measure_to_view_map = mock.Mock()
+        test_key = 'testKey'
+        test_value = 1
+        measurement_map = measurement_map_module.MeasurementMap(
+            measure_to_view_map=measure_to_view_map)
+        measurement_map.measure_int_put(test_key, test_value)
+
+        self.assertEqual({'testKey': 1}, measurement_map.measurement_map)
+
+    def test_measure_float_put(self):
+        measure_to_view_map = mock.Mock()
+        test_key = 'testKey'
+        test_value = 1.0
+        measurement_map = measurement_map_module.MeasurementMap(
+            measure_to_view_map=measure_to_view_map)
+        measurement_map.measure_float_put(test_key, test_value)
+
+        self.assertEqual({'testKey': 1.0}, measurement_map.measurement_map)
+
+    def test_record(self):
+        measure_to_view_map = mock.Mock()
+        measurement_map = measurement_map_module.MeasurementMap(
+            measure_to_view_map=measure_to_view_map)
+
+        tags = {'testtag1': 'testtag1val'}
+        measurement_map.record(tag_map_tags=tags)
+        self.assertTrue(measure_to_view_map.MeasureToViewMap.record.called)
diff --git a/tests/unit/stats/test_stats.py b/tests/unit/stats/test_stats.py
new file mode 100644
index 00000000..86dfd16
--- /dev/null
+++ b/tests/unit/stats/test_stats.py
@@ -0,0 +1,28 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import mock
+from opencensus.stats import stats as stats_module
+from opencensus.stats.view_manager import ViewManager
+from opencensus.stats.stats_recorder import StatsRecorder
+
+
+class TestStats(unittest.TestCase):
+
+    def test_constructor(self):
+        stats = stats_module.Stats()
+
+        self.assertEqual(stats._view_manager, stats.view_manager)
+        self.assertEqual(stats._stats_recorder, stats.stats_recorder)
diff --git a/tests/unit/stats/test_stats_recorder.py b/tests/unit/stats/test_stats_recorder.py
new file mode 100644
index 00000000..c023c66
--- /dev/null
+++ b/tests/unit/stats/test_stats_recorder.py
@@ -0,0 +1,40 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import mock
+from opencensus.stats import stats_recorder as stats_recorder_module
+from opencensus.stats.measurement_map import MeasurementMap
+
+
+class TestStatsRecorder(unittest.TestCase):
+
+    def test_constructor_defaults(self):
+        stats_recorder = stats_recorder_module.StatsRecorder()
+
+        self.assertEqual({}, stats_recorder.measure_to_view_map)
+
+    def test_constructor_explicit(self):
+        measure_to_view_map = mock.Mock()
+        stats_recorder = stats_recorder_module.StatsRecorder(measure_to_view_map=measure_to_view_map)
+
+        self.assertEqual(measure_to_view_map, stats_recorder.measure_to_view_map)
+
+    def test_new_measurement_map(self):
+        measure_to_view_map = mock.Mock()
+        stats_recorder = stats_recorder_module.StatsRecorder(measure_to_view_map=measure_to_view_map)
+        measurement_map = stats_recorder.new_measurement_map()
+
+        self.assertEqual(measurement_map.measure_to_view_map, MeasurementMap(measure_to_view_map=measure_to_view_map).measure_to_view_map)
+        self.assertEqual(measurement_map.measurement_map, MeasurementMap(measure_to_view_map=measure_to_view_map).measurement_map)
diff --git a/tests/unit/stats/test_view.py b/tests/unit/stats/test_view.py
new file mode 100644
index 00000000..1244ea9
--- /dev/null
+++ b/tests/unit/stats/test_view.py
@@ -0,0 +1,35 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import mock
+from opencensus.stats import view as view_module
+
+
+class TestView(unittest.TestCase):
+
+    def test_constructor(self):
+        name = "testName"
+        description = "testMeasure"
+        columns = ["testTagKey1", "testTagKey2"]
+        measure = mock.Mock()
+        aggregation = mock.Mock()
+
+        view = view_module.View(name=name, description=description, columns=columns, measure=measure, aggregation=aggregation)
+
+        self.assertEqual("testName", view.name)
+        self.assertEqual("testMeasure", view.description)
+        self.assertEqual(["testTagKey1", "testTagKey2"], view.columns)
+        self.assertEqual(measure, view.measure)
+        self.assertEqual(aggregation, view.aggregation)
diff --git a/tests/unit/stats/test_view_data.py b/tests/unit/stats/test_view_data.py
new file mode 100644
index 00000000..24d9b02
--- /dev/null
+++ b/tests/unit/stats/test_view_data.py
@@ -0,0 +1,129 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import mock
+from datetime import datetime
+from opencensus.stats import view_data as view_data_module
+
+
+class TestViewData(unittest.TestCase):
+
+    def test_constructor(self):
+        view = mock.Mock()
+        start_time = datetime.utcnow()
+        end_time = datetime.utcnow()
+        view_data = view_data_module.ViewData(view=view, start_time=start_time, end_time=end_time)
+
+        self.assertEqual(view, view_data.view)
+        self.assertEqual(start_time, view_data.start_time)
+        self.assertEqual(end_time, view_data.end_time)
+        self.assertEqual({}, view_data.tag_value_aggregation_map)
+
+    def test_start(self):
+        view = mock.Mock()
+        start_time = mock.Mock()
+        end_time = datetime.utcnow()
+        view_data = view_data_module.ViewData(view=view,
+                                              start_time=start_time,
+                                              end_time=end_time)
+        view_data.start()
+
+        self.assertIsNotNone(view_data.start_time)
+
+    def test_end(self):
+        view = mock.Mock()
+        start_time = datetime.utcnow()
+        end_time = mock.Mock()
+        view_data = view_data_module.ViewData(view=view,
+                                              start_time=start_time,
+                                              end_time=end_time)
+        view_data.end()
+
+        self.assertIsNotNone(view_data.end_time)
+
+    def test_get_tag_map(self):
+        view = mock.Mock()
+        start_time = datetime.utcnow()
+        end_time = datetime.utcnow()
+        view_data = view_data_module.ViewData(view=view,
+                                              start_time=start_time,
+                                              end_time=end_time)
+        test_context_1 = {'key1': 'val1'}
+        context_map_1 = view_data.get_tag_map(context=test_context_1)
+        self.assertEqual(test_context_1, view_data.tag_map)
+        self.assertEqual(test_context_1, context_map_1)
+
+        test_context_2 = {'key1': 'val2'}
+        context_map_2 = view_data.get_tag_map(context=test_context_2)
+        self.assertEqual(test_context_2, context_map_2)
+
+        test_context_3 = {}
+        context_map_3 = view_data.get_tag_map(context=test_context_3)
+        self.assertEqual({'key1': 'val2'}, context_map_3)
+
+    def test_get_tag_values(self):
+        view = mock.Mock()
+        start_time = datetime.utcnow()
+        end_time = datetime.utcnow()
+        view_data = view_data_module.ViewData(view=view,
+                                              start_time=start_time,
+                                              end_time=end_time)
+
+        tags = {'testTag1': 'testVal1'}
+        columns = ['testTag1']
+        tag_values = view_data.get_tag_values(tags, columns)
+        self.assertEqual(['testVal1'], tag_values)
+
+        tags = {'testTag1': 'testVal1'}
+        columns = ['testTag2']
+        tag_values = view_data.get_tag_values(tags, columns)
+        self.assertEqual([None], tag_values)
+
+    def test_record(self):
+        view = mock.Mock()
+        view.columns = ['key1']
+        view.aggregation = mock.Mock()
+        start_time = datetime.utcnow()
+        end_time = datetime.utcnow()
+        view_data = view_data_module.ViewData(view=view,
+                                              start_time=start_time,
+                                              end_time=end_time)
+
+        context = {'key1': 'val1', 'key2': 'val2'}
+        time = datetime.utcnow().isoformat() + 'Z'
+        value = 1
+        self.assertEqual({}, view_data.tag_value_aggregation_map)
+
+        view_data.record(context=context, value=value, timestamp=time)
+        tag_values = view_data.get_tag_values(
+            tags=view_data.get_tag_map(context=context), columns=view.columns)
+        tuple_vals = tuple(tag_values)
+        self.assertEqual(['val1'], tag_values)
+        self.assertIsNotNone(view_data.tag_value_aggregation_map)
+
+        self.assertTrue('val1' in view_data.tag_value_aggregation_map)
+        self.assertTrue('val1' in tag_values)
+        self.assertIsNotNone(view_data.tag_value_aggregation_map['val1'])
+        self.assertIsNotNone(view_data.tag_value_aggregation_map.get(
+            'val1').add(value))
+
+        view_data.record(context=context, value=value, timestamp=time)
+        tag_values.append('val2')
+        self.assertFalse('val2' in view_data.tag_value_aggregation_map)
+        view_data.tag_value_aggregation_map['val2'] = view.aggregation
+        self.assertEqual(view_data.tag_value_aggregation_map.get('val2'),
+                         view_data.view.aggregation)
+        self.assertIsNotNone(view_data.tag_value_aggregation_map.get(
+            'val2').add(value))
diff --git a/tests/unit/stats/test_view_manager.py b/tests/unit/stats/test_view_manager.py
new file mode 100644
index 00000000..d57844f
--- /dev/null
+++ b/tests/unit/stats/test_view_manager.py
@@ -0,0 +1,58 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import mock
+from opencensus.stats import view_manager as view_manager_module
+
+
+class TestViewManager(unittest.TestCase):
+    def test_constructor(self):
+        view_manager = view_manager_module.ViewManager()
+
+        self.assertIsNotNone(view_manager.time)
+        self.assertIsNotNone(view_manager.measure_to_view_map)
+
+    def test_register_view(self):
+        view = mock.Mock()
+        view_manager = view_manager_module.ViewManager()
+        view_manager.register_view(view=view)
+
+        view_manager_mock = mock.Mock()
+        view_manager = view_manager_mock
+
+        view_manager.register_view(view=mock.Mock())
+        self.assertTrue(view_manager_mock.register_view.called)
+
+    def test_get_view(self):
+        view_name = mock.Mock()
+        view_manager = view_manager_module.ViewManager()
+        view_manager.get_view(view_name=view_name)
+
+        view_manager_mock = mock.Mock()
+        view_manager = view_manager_mock
+
+        view_manager.get_view(view_name=mock.Mock())
+        self.assertTrue(view_manager_mock.get_view.called)
+
+    def test_get_all_exported_views(self):
+        view_manager = view_manager_module.ViewManager()
+        exported_views = view_manager.get_all_exported_views()
+        self.assertIsNotNone(exported_views)
+
+        view_manager_mock = mock.Mock()
+        view_manager = view_manager_mock
+
+        view_manager.get_all_exported_views()
+        self.assertTrue(view_manager_mock.get_all_exported_views.called)
diff --git a/tests/unit/tags/test_tag.py b/tests/unit/tags/test_tag.py
new file mode 100644
index 00000000..238124a
--- /dev/null
+++ b/tests/unit/tags/test_tag.py
@@ -0,0 +1,27 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+from opencensus.tags import tag as tag_module
+
+
+class TestTag(unittest.TestCase):
+
+    def test_constructor(self):
+        key = 'key1'
+        value = 'value1'
+        tag = tag_module.Tag(key=key, value=value)
+
+        self.assertEqual(tag.key.name, key)
+        self.assertEqual(tag.value.value, value)
diff --git a/tests/unit/tags/test_tag_key.py b/tests/unit/tags/test_tag_key.py
new file mode 100644
index 00000000..5a1d778
--- /dev/null
+++ b/tests/unit/tags/test_tag_key.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+from opencensus.tags import tag_key as tag_key_module
+
+
+class TestTagKey(unittest.TestCase):
+
+    def test_constructor(self):
+        key = 'key1'
+        tag_key = tag_key_module.TagKey(key)
+
+        self.assertEqual(tag_key.name, key)
+
+    def test_is_valid(self):
+        test_key3 = 'e9nnb1ixRnvzBH1TUonCG5IsV3ba2PMKjAbSxdLFFpgxFKhZHfi92ajNH6EARaK9FGGShk2EeZ4XObwqIPBwi7j4ZSRR1ZWXtS15keA1h4c9CxeAdakcxxUN0YH6mLJ0BygwRbdbMSeOIPWLo7iyGCil4njKOxH6HF7k0aN4BQl03HQZoXe0t0gd5xKQW37ePNA4FRVZlbLbib3GCF7BeKeA0DKMtuRu27r2hDGEFAmvqh3JEnqOy4gDbhFubaLblr4R4GOHo'
+        tag_key1 = tag_key_module.TagKey('')
+        self.assertFalse(tag_key1.is_valid_name(tag_key1.name))
+        tag_key2 = tag_key_module.TagKey('testKey')
+        self.assertTrue(tag_key2.is_valid_name(tag_key2.name))
+        tag_key3 = tag_key_module.TagKey(test_key3)
+        self.assertFalse(tag_key3.is_valid_name(tag_key3.name))
+        tag_key4 = tag_key_module.TagKey('Æ!01kr')
+        self.assertFalse(tag_key3.is_valid_name(tag_key4.name))
diff --git a/tests/unit/tags/test_tag_map.py b/tests/unit/tags/test_tag_map.py
new file mode 100644
index 00000000..7459f27
--- /dev/null
+++ b/tests/unit/tags/test_tag_map.py
@@ -0,0 +1,75 @@
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import mock
+from opencensus.tags import tag_map as tag_map_module
+
+
+class TestTagMap(unittest.TestCase):
+
+    def test_constructor_defaults(self):
+        tag_map = tag_map_module.TagMap()
+        self.assertEqual(tag_map.tags, {})
+        self.assertEqual(tag_map.map, {})
+
+    def test_constructor_explicit(self):
+        tags = [{'key1': 'value1'}]
+
+        tag_map = tag_map_module.TagMap(tags=tags)
+        self.assertEqual(tag_map.tags, tags)
+
+    def test_insert(self):
+        test_key = 'key1'
+        test_value = 'value1'
+        tag_map = tag_map_module.TagMap()
+        tag_map.insert(key=test_key, value=test_value)
+        self.assertEqual({'key1': 'value1'}, tag_map.map)
+        tag_map.insert(key=test_key, value=test_value)
+        self.assertEqual({'key1': 'value1'}, tag_map.map)
+
+    def test_delete(self):
+        key = 'key1'
+        tag_map = tag_map_module.TagMap(tags=[{'key1': 'value1', 'key2': 'value2'}])
+        tag_map.delete(key=key)
+        self.assertEqual(tag_map.map, {'key2': 'value2'})
+
+    def test_update(self):
+        key = 'key1'
+        value = 'value1'
+        tag_map = tag_map_module.TagMap(tags=[{'key1': 'value2'}])
+        tag_map.update(key=key, value=value)
+        self.assertEqual({'key1': 'value1'}, tag_map.map)
+        key_2 = 'key2'
+        tag_map.update(key=key_2, value=value)
+        self.assertEqual({'key1': 'value1'}, tag_map.map)
+
+    def test_tag_key_exists(self):
+        key = mock.Mock()
+        value = mock.Mock()
+        tag_map = tag_map_module.TagMap(tags=[{key: value}])
+        self.assertTrue(tag_map.tag_key_exists(key))
+        self.assertFalse(tag_map.tag_key_exists('nokey'))
+
+    def test_value(self):
+        key = 'key1'
+        value = 'value1'
+        tag_map = tag_map_module.TagMap(tags=[{key: value}])
+        test_val = tag_map.get_value(key)
+        self.assertEqual(test_val, value)
+
+        value_1 = None
+        tag_map = tag_map_module.TagMap(tags=[{key: value_1}])
+        with self.assertRaises(KeyError):
+            tag_map.get_value(key=key)
diff --git a/tests/unit/tags/test_tag_value.py b/tests/unit/tags/test_tag_value.py
new file mode 100644
index 00000000..6bc8c85
--- /dev/null
+++ b/tests/unit/tags/test_tag_value.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2018, OpenCensus Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+from opencensus.tags import tag_value as tag_value_module
+
+
+class TestTagValue(unittest.TestCase):
+
+    def test_constructor(self):
+        value = 'value1'
+        tag_value = tag_value_module.TagValue(value=value)
+
+        self.assertEqual(tag_value.value, value)
+
+    def test_check_value(self):
+        test_val1 = 'e9nnb1ixRnvzBH1TUonCG5IsV3ba2PMKjAbSxdLFFpgxFKhZHfi92ajNH6EARaK9FGGShk2EeZ4XObwqIPBwi7j4ZSRR1ZWXtS15keA1h4c9CxeAdakcxxUN0YH6mLJ0BygwRbdbMSeOIPWLo7iyGCil4njKOxH6HF7k0aN4BQl03HQZoXe0t0gd5xKQW37ePNA4FRVZlbLbib3GCF7BeKeA0DKMtuRu27r2hDGEFAmvqh3JEnqOy4gDbhFubaLblr4R4GOHo'
+        tag_val1 = tag_value_module.TagValue(test_val1)
+        self.assertFalse(tag_val1.is_valid_value(tag_val1.value))
+
+        tag_val2 = tag_value_module.TagValue('testVal')
+        self.assertTrue(tag_val2.is_valid_value(tag_val2.value))
+
+        tag_val3 = tag_value_module.TagValue('Æ!01kr')
+        self.assertFalse(tag_val3.is_valid_value(tag_val3.value))